mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-02-22 12:21:39 +00:00
feat: condition node functionality with CEL evaluation in Workflows (#2280)
* feat: add condition node functionality with CEL evaluation - Introduced ConditionNode to support conditional branching in workflows. - Implemented CEL evaluation for state updates and condition expressions. - Updated WorkflowEngine to handle condition nodes and their execution logic. - Enhanced validation for workflows to ensure condition nodes have at least two outgoing edges and valid expressions. - Modified frontend components to support new condition node type and its configuration. - Added necessary types and interfaces for condition cases and state operations. - Updated requirements to include cel-python for expression evaluation. * mini-fixes * feat(workflow): improve UX --------- Co-authored-by: Alex <a@tushynski.me>
This commit is contained in:
@@ -5,7 +5,7 @@ interface BaseNodeProps {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
selected?: boolean;
|
||||
type?: 'start' | 'end' | 'default' | 'state' | 'agent';
|
||||
type?: 'start' | 'end' | 'default' | 'state' | 'agent' | 'condition';
|
||||
icon?: ReactNode;
|
||||
handles?: {
|
||||
source?: boolean;
|
||||
@@ -40,6 +40,9 @@ export const BaseNode: React.FC<BaseNodeProps> = ({
|
||||
} else if (type === 'state') {
|
||||
iconBg = 'bg-gray-100 dark:bg-gray-800';
|
||||
iconColor = 'text-gray-600 dark:text-gray-400';
|
||||
} else if (type === 'condition') {
|
||||
iconBg = 'bg-orange-100 dark:bg-orange-900/30';
|
||||
iconColor = 'text-orange-600 dark:text-orange-400';
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
118
frontend/src/agents/workflow/nodes/ConditionNode.tsx
Normal file
118
frontend/src/agents/workflow/nodes/ConditionNode.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { GitBranch } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
|
||||
import { ConditionCase } from '../../types/workflow';
|
||||
|
||||
type ConditionNodeData = {
|
||||
label?: string;
|
||||
title?: string;
|
||||
config?: {
|
||||
mode?: 'simple' | 'advanced';
|
||||
cases?: ConditionCase[];
|
||||
};
|
||||
};
|
||||
|
||||
const ROW_HEIGHT = 18;
|
||||
const HEADER_HEIGHT = 52;
|
||||
const PADDING_BOTTOM = 8;
|
||||
|
||||
function getNodeHeight(caseCount: number): number {
|
||||
return (
|
||||
HEADER_HEIGHT + Math.max(caseCount + 1, 2) * ROW_HEIGHT + PADDING_BOTTOM
|
||||
);
|
||||
}
|
||||
|
||||
function getHandleTop(index: number, total: number): string {
|
||||
const offset = HEADER_HEIGHT;
|
||||
return `${offset + ROW_HEIGHT * index + ROW_HEIGHT / 2}px`;
|
||||
}
|
||||
|
||||
const ConditionNode = ({ data, selected }: NodeProps<ConditionNodeData>) => {
|
||||
const title = data.title || data.label || 'If / Else';
|
||||
const cases = data.config?.cases || [];
|
||||
const totalOutputs = cases.length + 1;
|
||||
const height = getNodeHeight(cases.length);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative rounded-2xl border bg-white shadow-md transition-all dark:bg-[#2C2C2C] ${
|
||||
selected
|
||||
? 'border-violets-are-blue dark:ring-violets-are-blue scale-105 ring-2 ring-purple-300'
|
||||
: 'border-gray-200 hover:shadow-lg dark:border-[#3A3A3A]'
|
||||
}`}
|
||||
style={{ minWidth: 180, maxWidth: 220, height }}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
className="hover:bg-violets-are-blue! top-1/2! -left-1! h-3! w-3! rounded-full! border-2! border-white! bg-gray-400! transition-colors dark:border-[#2C2C2C]!"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-3 px-3 py-2">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400">
|
||||
<GitBranch size={14} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 pr-2">
|
||||
<div
|
||||
className="truncate text-sm font-semibold text-gray-900 dark:text-white"
|
||||
title={title}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-[10px] text-gray-500 uppercase">
|
||||
{data.config?.mode || 'simple'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-3">
|
||||
{cases.map((c, i) => (
|
||||
<div
|
||||
key={c.sourceHandle}
|
||||
className="flex items-center gap-1"
|
||||
style={{ height: ROW_HEIGHT }}
|
||||
>
|
||||
<span className="shrink-0 text-xs font-medium text-orange-600 dark:text-orange-400">
|
||||
{i === 0 ? 'If' : 'Else if'}
|
||||
</span>
|
||||
{c.name && (
|
||||
<span
|
||||
className="truncate text-xs text-gray-600 dark:text-gray-400"
|
||||
title={c.name}
|
||||
>
|
||||
{c.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1" style={{ height: ROW_HEIGHT }}>
|
||||
<span className="text-xs font-medium text-gray-500">Else</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cases.map((c, i) => (
|
||||
<Handle
|
||||
key={c.sourceHandle}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={c.sourceHandle}
|
||||
isConnectable
|
||||
style={{ top: getHandleTop(i, totalOutputs) }}
|
||||
className="hover:bg-violets-are-blue! -right-1! h-3! w-3! rounded-full! border-2! border-white! bg-orange-400! transition-colors dark:border-[#2C2C2C]!"
|
||||
/>
|
||||
))}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="else"
|
||||
isConnectable
|
||||
style={{ top: getHandleTop(cases.length, totalOutputs) }}
|
||||
className="hover:bg-violets-are-blue! -right-1! h-3! w-3! rounded-full! border-2! border-white! bg-gray-400! transition-colors dark:border-[#2C2C2C]!"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ConditionNode);
|
||||
@@ -2,6 +2,7 @@ import { Database } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
|
||||
import { StateOperationConfig } from '../../types/workflow';
|
||||
import { BaseNode } from './BaseNode';
|
||||
|
||||
type SetStateNodeData = {
|
||||
@@ -9,10 +10,16 @@ type SetStateNodeData = {
|
||||
title?: string;
|
||||
variable?: string;
|
||||
value?: string;
|
||||
config?: {
|
||||
operations?: StateOperationConfig[];
|
||||
};
|
||||
};
|
||||
|
||||
const SetStateNode = ({ data, selected }: NodeProps<SetStateNodeData>) => {
|
||||
const title = data.title || data.label || 'Set State';
|
||||
const operations = data.config?.operations || [];
|
||||
const hasLegacy = !operations.length && data.variable;
|
||||
|
||||
return (
|
||||
<BaseNode
|
||||
title={title}
|
||||
@@ -22,22 +29,31 @@ const SetStateNode = ({ data, selected }: NodeProps<SetStateNodeData>) => {
|
||||
handles={{ source: true, target: true }}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
{data.variable && (
|
||||
{operations.length > 0 ? (
|
||||
<div
|
||||
className="truncate text-[10px] text-gray-500 uppercase"
|
||||
title={`Variable: ${data.variable}`}
|
||||
className="truncate text-[10px] text-gray-500"
|
||||
title={`${operations.length} operation(s)`}
|
||||
>
|
||||
{data.variable}
|
||||
{operations.length} variable{operations.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
)}
|
||||
{data.value && (
|
||||
<div
|
||||
className="truncate text-xs text-blue-600 dark:text-blue-400"
|
||||
title={`Value: ${data.value}`}
|
||||
>
|
||||
{data.value}
|
||||
</div>
|
||||
)}
|
||||
) : hasLegacy ? (
|
||||
<>
|
||||
<div
|
||||
className="truncate text-[10px] text-gray-500 uppercase"
|
||||
title={`Variable: ${data.variable}`}
|
||||
>
|
||||
{data.variable}
|
||||
</div>
|
||||
{data.value && (
|
||||
<div
|
||||
className="truncate text-xs text-blue-600 dark:text-blue-400"
|
||||
title={`Value: ${data.value}`}
|
||||
>
|
||||
{data.value}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</BaseNode>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Bot, Flag, Play, StickyNote } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { BaseNode } from './BaseNode';
|
||||
import ConditionNode from './ConditionNode';
|
||||
import SetStateNode from './SetStateNode';
|
||||
import { Play, Bot, StickyNote, Flag } from 'lucide-react';
|
||||
|
||||
export const StartNode = memo(function StartNode({
|
||||
selected,
|
||||
@@ -142,3 +144,4 @@ export const NoteNode = memo(function NoteNode({
|
||||
});
|
||||
|
||||
export { SetStateNode };
|
||||
export { ConditionNode };
|
||||
|
||||
Reference in New Issue
Block a user