feat: agent workflow builder (#2264)

* feat: implement WorkflowAgent and GraphExecutor for workflow management and execution

* refactor: workflow schemas and introduce WorkflowEngine

- Updated schemas in `schemas.py` to include new agent types and configurations.
- Created `WorkflowEngine` class in `workflow_engine.py` to manage workflow execution.
- Enhanced `StreamProcessor` to handle workflow-related data.
- Added new routes and utilities for managing workflows in the user API.
- Implemented validation and serialization functions for workflows.
- Established MongoDB collections and indexes for workflows and related entities.

* refactor: improve WorkflowAgent documentation and update type hints in WorkflowEngine

* feat: workflow builder and managing in frontend

- Added new endpoints for workflows in `endpoints.ts`.
- Implemented `getWorkflow`, `createWorkflow`, and `updateWorkflow` methods in `userService.ts`.
- Introduced new UI components for alerts, buttons, commands, dialogs, multi-select, popovers, and selects.
- Enhanced styling in `index.css` with new theme variables and animations.
- Refactored modal components for better layout and styling.
- Configured TypeScript paths and Vite aliases for cleaner imports.

* feat: add workflow preview component and related state management

- Implemented WorkflowPreview component for displaying workflow execution.
- Created WorkflowPreviewSlice for managing workflow preview state, including queries and execution steps.
- Added WorkflowMiniMap for visual representation of workflow nodes and their statuses.
- Integrated conversation handling with the ability to fetch answers and manage query states.
- Introduced reusable Sheet component for UI overlays.
- Updated Redux store to include workflowPreview reducer.

* feat: enhance workflow execution details and state management in WorkflowEngine and WorkflowPreview

* feat: enhance workflow components with improved UI and functionality

- Updated WorkflowPreview to allow text truncation for better display of long names.
- Enhanced BaseNode with connectable handles and improved styling for better visibility.
- Added MobileBlocker component to inform users about desktop requirements for the Workflow Builder.
- Introduced PromptTextArea component for improved variable insertion and search functionality, including upstream variable extraction and context addition.

* feat(workflow): add owner validation and graph version support

* fix: ruff lint

---------

Co-authored-by: Alex <a@tushynski.me>
This commit is contained in:
Siddhant Rai
2026-02-11 19:45:24 +05:30
committed by GitHub
parent 8353f9c649
commit 8ef321d784
52 changed files with 8634 additions and 222 deletions

View File

@@ -0,0 +1,91 @@
import React, { ReactNode } from 'react';
import { Handle, Position } from 'reactflow';
interface BaseNodeProps {
title: string;
children?: ReactNode;
selected?: boolean;
type?: 'start' | 'end' | 'default' | 'state' | 'agent';
icon?: ReactNode;
handles?: {
source?: boolean;
target?: boolean;
};
}
export const BaseNode: React.FC<BaseNodeProps> = ({
title,
children,
selected,
type = 'default',
icon,
handles = { source: true, target: true },
}) => {
let bgColor = 'bg-white dark:bg-[#2C2C2C]';
let borderColor = 'border-gray-200 dark:border-[#3A3A3A]';
let iconBg = 'bg-gray-100 dark:bg-gray-800';
let iconColor = 'text-gray-600 dark:text-gray-400';
if (selected) {
borderColor =
'border-violets-are-blue ring-2 ring-purple-300 dark:ring-violets-are-blue';
}
if (type === 'start') {
iconBg = 'bg-green-100 dark:bg-green-900/30';
iconColor = 'text-green-600 dark:text-green-400';
} else if (type === 'end') {
iconBg = 'bg-red-100 dark:bg-red-900/30';
iconColor = 'text-red-600 dark:text-red-400';
} else if (type === 'state') {
iconBg = 'bg-gray-100 dark:bg-gray-800';
iconColor = 'text-gray-600 dark:text-gray-400';
}
return (
<div
className={`rounded-full border ${bgColor} ${borderColor} shadow-md transition-all hover:shadow-lg ${
selected ? 'scale-105' : ''
} max-w-[250px] min-w-[180px]`}
>
{handles.target && (
<Handle
type="target"
position={Position.Left}
isConnectable={true}
className="hover:bg-violets-are-blue! -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-4 py-3">
<div
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-full ${iconBg} ${iconColor}`}
>
{icon}
</div>
<div className="min-w-0 flex-1 pr-3">
<div
className="truncate text-sm font-semibold text-gray-900 dark:text-white"
title={title}
>
{title}
</div>
{children && (
<div className="mt-1 truncate text-xs text-gray-600 dark:text-gray-400">
{children}
</div>
)}
</div>
</div>
{handles.source && (
<Handle
type="source"
position={Position.Right}
isConnectable={true}
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>
);
};

View File

@@ -0,0 +1,46 @@
import { Database } from 'lucide-react';
import { memo } from 'react';
import { NodeProps } from 'reactflow';
import { BaseNode } from './BaseNode';
type SetStateNodeData = {
label?: string;
title?: string;
variable?: string;
value?: string;
};
const SetStateNode = ({ data, selected }: NodeProps<SetStateNodeData>) => {
const title = data.title || data.label || 'Set State';
return (
<BaseNode
title={title}
type="state"
selected={selected}
icon={<Database size={16} />}
handles={{ source: true, target: true }}
>
<div className="flex flex-col gap-1">
{data.variable && (
<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>
)}
</div>
</BaseNode>
);
};
export default memo(SetStateNode);

View File

@@ -0,0 +1,144 @@
import React, { memo } from 'react';
import { BaseNode } from './BaseNode';
import SetStateNode from './SetStateNode';
import { Play, Bot, StickyNote, Flag } from 'lucide-react';
export const StartNode = memo(function StartNode({
selected,
}: {
selected: boolean;
}) {
return (
<BaseNode
title="Start"
type="start"
selected={selected}
handles={{ target: false, source: true }}
icon={<Play size={16} />}
>
<div className="text-xs text-gray-500">Entry point of the workflow</div>
</BaseNode>
);
});
export const EndNode = memo(function EndNode({
selected,
}: {
selected: boolean;
}) {
return (
<BaseNode
title="End"
type="end"
selected={selected}
handles={{ target: true, source: false }}
icon={<Flag size={16} />}
>
<div className="text-xs text-gray-500">Workflow completion</div>
</BaseNode>
);
});
export const AgentNode = memo(function AgentNode({
data,
selected,
}: {
data: {
title?: string;
label?: string;
config?: {
agent_type?: string;
model_id?: string;
prompt_template?: string;
output_variable?: string;
};
};
selected: boolean;
}) {
const title = data.title || data.label || 'Agent';
const config = data.config || {};
return (
<BaseNode
title={title}
type="agent"
selected={selected}
icon={<Bot size={16} />}
>
<div className="flex flex-col gap-1">
{config.agent_type && (
<div
className="truncate text-[10px] text-gray-500 uppercase"
title={config.agent_type}
>
{config.agent_type}
</div>
)}
{config.model_id && (
<div
className="text-purple-30 dark:text-violets-are-blue truncate text-xs"
title={config.model_id}
>
{config.model_id}
</div>
)}
{config.output_variable && (
<div
className="truncate text-xs text-green-600 dark:text-green-400"
title={`Output ➔ ${config.output_variable}`}
>
Output {config.output_variable}
</div>
)}
</div>
</BaseNode>
);
});
export const NoteNode = memo(function NoteNode({
data,
selected,
}: {
data: { title?: string; label?: string; content?: string };
selected: boolean;
}) {
const title = data.title || data.label || 'Note';
const maxContentLength = 120;
const displayContent =
data.content && data.content.length > maxContentLength
? `${data.content.substring(0, maxContentLength)}...`
: data.content;
return (
<div
className={`max-w-[250px] rounded-3xl border border-yellow-200 bg-yellow-50 px-5 py-3 shadow-md transition-all dark:border-yellow-800 dark:bg-yellow-900/20 ${
selected
? 'scale-105 ring-2 ring-yellow-300 dark:ring-yellow-700'
: 'hover:shadow-lg'
}`}
>
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-yellow-100 text-yellow-700 dark:bg-yellow-800/30 dark:text-yellow-500">
<StickyNote size={18} />
</div>
<div className="min-w-0 flex-1">
<div
className="truncate text-sm font-semibold text-yellow-800 dark:text-yellow-300"
title={title}
>
{title}
</div>
{displayContent && (
<div
className="mt-1 text-xs wrap-break-word text-yellow-700 italic dark:text-yellow-400"
title={data.content}
>
{displayContent}
</div>
)}
</div>
</div>
</div>
);
});
export { SetStateNode };