mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-02-22 04:11:41 +00:00
* 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>
442 lines
13 KiB
TypeScript
442 lines
13 KiB
TypeScript
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
|
|
import conversationService from '../../api/services/conversationService';
|
|
import { Query, Status } from '../../conversation/conversationModels';
|
|
import { WorkflowEdge, WorkflowNode } from '../types/workflow';
|
|
|
|
export interface WorkflowExecutionStep {
|
|
nodeId: string;
|
|
nodeType: string;
|
|
nodeTitle: string;
|
|
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
reasoning?: string;
|
|
startedAt?: number;
|
|
completedAt?: number;
|
|
stateSnapshot?: Record<string, unknown>;
|
|
output?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface WorkflowData {
|
|
name: string;
|
|
description?: string;
|
|
nodes: WorkflowNode[];
|
|
edges: WorkflowEdge[];
|
|
}
|
|
|
|
export interface WorkflowQuery extends Query {
|
|
executionSteps?: WorkflowExecutionStep[];
|
|
}
|
|
|
|
export interface WorkflowPreviewState {
|
|
queries: WorkflowQuery[];
|
|
status: Status;
|
|
executionSteps: WorkflowExecutionStep[];
|
|
activeNodeId: string | null;
|
|
}
|
|
|
|
const initialState: WorkflowPreviewState = {
|
|
queries: [],
|
|
status: 'idle',
|
|
executionSteps: [],
|
|
activeNodeId: null,
|
|
};
|
|
|
|
let abortController: AbortController | null = null;
|
|
|
|
export function handleWorkflowPreviewAbort() {
|
|
if (abortController) {
|
|
abortController.abort();
|
|
abortController = null;
|
|
}
|
|
}
|
|
|
|
interface ThunkState {
|
|
preference: {
|
|
token: string | null;
|
|
};
|
|
workflowPreview: WorkflowPreviewState;
|
|
}
|
|
|
|
export const fetchWorkflowPreviewAnswer = createAsyncThunk<
|
|
void,
|
|
{
|
|
question: string;
|
|
workflowData: WorkflowData;
|
|
indx?: number;
|
|
},
|
|
{ state: ThunkState }
|
|
>(
|
|
'workflowPreview/fetchAnswer',
|
|
async ({ question, workflowData, indx }, { dispatch, getState }) => {
|
|
if (abortController) abortController.abort();
|
|
abortController = new AbortController();
|
|
const { signal } = abortController;
|
|
|
|
const state = getState();
|
|
|
|
if (state.preference) {
|
|
const payload = {
|
|
question,
|
|
workflow: workflowData,
|
|
save_conversation: false,
|
|
};
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
conversationService
|
|
.answerStream(payload, state.preference.token, signal)
|
|
.then((response) => {
|
|
if (!response.body) throw Error('No response body');
|
|
|
|
let buffer = '';
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder('utf-8');
|
|
|
|
const processStream = ({
|
|
done,
|
|
value,
|
|
}: ReadableStreamReadResult<Uint8Array>): Promise<void> | void => {
|
|
if (done) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
|
|
const currentState = getState();
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data:')) {
|
|
try {
|
|
const data = JSON.parse(line.slice(5));
|
|
const targetIndex =
|
|
indx ?? currentState.workflowPreview.queries.length - 1;
|
|
|
|
if (data.type === 'end') {
|
|
dispatch(workflowPreviewSlice.actions.setStatus('idle'));
|
|
} else if (data.type === 'thought') {
|
|
dispatch(
|
|
updateThought({
|
|
index: targetIndex,
|
|
query: { thought: data.thought },
|
|
}),
|
|
);
|
|
} else if (data.type === 'workflow_step') {
|
|
dispatch(
|
|
updateExecutionStep({
|
|
index: targetIndex,
|
|
step: {
|
|
nodeId: data.node_id,
|
|
nodeType: data.node_type,
|
|
nodeTitle: data.node_title,
|
|
status: data.status,
|
|
reasoning: data.reasoning,
|
|
stateSnapshot: data.state_snapshot,
|
|
output: data.output,
|
|
error: data.error,
|
|
},
|
|
}),
|
|
);
|
|
if (data.status === 'running') {
|
|
dispatch(setActiveNodeId(data.node_id));
|
|
}
|
|
} else if (data.type === 'source') {
|
|
dispatch(
|
|
updateStreamingSource({
|
|
index: targetIndex,
|
|
query: { sources: data.source ?? [] },
|
|
}),
|
|
);
|
|
} else if (data.type === 'tool_call') {
|
|
dispatch(
|
|
updateToolCall({
|
|
index: targetIndex,
|
|
tool_call: data.data,
|
|
}),
|
|
);
|
|
} else if (data.type === 'error') {
|
|
dispatch(
|
|
workflowPreviewSlice.actions.setStatus('failed'),
|
|
);
|
|
dispatch(
|
|
workflowPreviewSlice.actions.raiseError({
|
|
index: targetIndex,
|
|
message: data.error,
|
|
}),
|
|
);
|
|
} else if (data.type === 'structured_answer') {
|
|
dispatch(
|
|
updateStreamingQuery({
|
|
index: targetIndex,
|
|
query: {
|
|
response: data.answer,
|
|
structured: data.structured,
|
|
schema: data.schema,
|
|
},
|
|
}),
|
|
);
|
|
} else if (data.answer !== undefined) {
|
|
dispatch(
|
|
updateStreamingQuery({
|
|
index: targetIndex,
|
|
query: { response: data.answer },
|
|
}),
|
|
);
|
|
}
|
|
} catch {
|
|
/* empty */
|
|
}
|
|
}
|
|
}
|
|
|
|
return reader.read().then(processStream);
|
|
};
|
|
|
|
reader.read().then(processStream).catch(reject);
|
|
})
|
|
.catch(reject);
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
export const workflowPreviewSlice = createSlice({
|
|
name: 'workflowPreview',
|
|
initialState,
|
|
reducers: {
|
|
addQuery(state, action: PayloadAction<Query>) {
|
|
state.queries.push(action.payload);
|
|
},
|
|
resendQuery(
|
|
state,
|
|
action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
|
|
) {
|
|
state.queries = [
|
|
...state.queries.slice(0, action.payload.index),
|
|
{ prompt: action.payload.prompt },
|
|
];
|
|
state.executionSteps = [];
|
|
state.activeNodeId = null;
|
|
},
|
|
updateStreamingQuery(
|
|
state,
|
|
action: PayloadAction<{
|
|
index: number;
|
|
query: Partial<Query>;
|
|
}>,
|
|
) {
|
|
const { index, query } = action.payload;
|
|
if (state.status === 'idle') return;
|
|
|
|
if (query.response !== undefined) {
|
|
state.queries[index].response =
|
|
(state.queries[index].response || '') + query.response;
|
|
}
|
|
|
|
if (query.structured !== undefined) {
|
|
state.queries[index].structured = query.structured;
|
|
}
|
|
|
|
if (query.schema !== undefined) {
|
|
state.queries[index].schema = query.schema;
|
|
}
|
|
},
|
|
updateThought(
|
|
state,
|
|
action: PayloadAction<{
|
|
index: number;
|
|
query: Partial<Query>;
|
|
}>,
|
|
) {
|
|
const { index, query } = action.payload;
|
|
if (query.thought !== undefined) {
|
|
state.queries[index].thought =
|
|
(state.queries[index].thought || '') + query.thought;
|
|
}
|
|
},
|
|
updateStreamingSource(
|
|
state,
|
|
action: PayloadAction<{
|
|
index: number;
|
|
query: Partial<Query>;
|
|
}>,
|
|
) {
|
|
const { index, query } = action.payload;
|
|
if (!state.queries[index].sources) {
|
|
state.queries[index].sources = query?.sources;
|
|
} else if (query.sources) {
|
|
state.queries[index].sources!.push(...query.sources);
|
|
}
|
|
},
|
|
updateToolCall(state, action) {
|
|
const { index, tool_call } = action.payload;
|
|
|
|
if (!state.queries[index].tool_calls) {
|
|
state.queries[index].tool_calls = [];
|
|
}
|
|
|
|
const existingIndex = state.queries[index].tool_calls.findIndex(
|
|
(call: { call_id: string }) => call.call_id === tool_call.call_id,
|
|
);
|
|
|
|
if (existingIndex !== -1) {
|
|
const existingCall = state.queries[index].tool_calls[existingIndex];
|
|
state.queries[index].tool_calls[existingIndex] = {
|
|
...existingCall,
|
|
...tool_call,
|
|
};
|
|
} else {
|
|
state.queries[index].tool_calls.push(tool_call);
|
|
}
|
|
},
|
|
updateQuery(
|
|
state,
|
|
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
|
) {
|
|
const { index, query } = action.payload;
|
|
state.queries[index] = {
|
|
...state.queries[index],
|
|
...query,
|
|
};
|
|
},
|
|
updateExecutionStep(
|
|
state,
|
|
action: PayloadAction<{
|
|
index: number;
|
|
step: Partial<WorkflowExecutionStep> & {
|
|
nodeId: string;
|
|
nodeType: string;
|
|
nodeTitle: string;
|
|
status: WorkflowExecutionStep['status'];
|
|
};
|
|
}>,
|
|
) {
|
|
const { index, step } = action.payload;
|
|
|
|
if (!state.queries[index]) return;
|
|
if (!state.queries[index].executionSteps) {
|
|
state.queries[index].executionSteps = [];
|
|
}
|
|
|
|
const querySteps = state.queries[index].executionSteps!;
|
|
const existingIndex = querySteps.findIndex((s) => s.nodeId === step.nodeId);
|
|
|
|
const updatedStep: WorkflowExecutionStep = {
|
|
nodeId: step.nodeId,
|
|
nodeType: step.nodeType,
|
|
nodeTitle: step.nodeTitle,
|
|
status: step.status,
|
|
reasoning: step.reasoning,
|
|
stateSnapshot: step.stateSnapshot,
|
|
output: step.output,
|
|
error: step.error,
|
|
startedAt: existingIndex !== -1 ? querySteps[existingIndex].startedAt : Date.now(),
|
|
completedAt:
|
|
step.status === 'completed' || step.status === 'failed'
|
|
? Date.now()
|
|
: existingIndex !== -1
|
|
? querySteps[existingIndex].completedAt
|
|
: undefined,
|
|
};
|
|
|
|
if (existingIndex !== -1) {
|
|
updatedStep.stateSnapshot = step.stateSnapshot ?? querySteps[existingIndex].stateSnapshot;
|
|
updatedStep.output = step.output ?? querySteps[existingIndex].output;
|
|
updatedStep.error = step.error ?? querySteps[existingIndex].error;
|
|
querySteps[existingIndex] = updatedStep;
|
|
} else {
|
|
querySteps.push(updatedStep);
|
|
}
|
|
|
|
const globalIndex = state.executionSteps.findIndex((s) => s.nodeId === step.nodeId);
|
|
if (globalIndex !== -1) {
|
|
state.executionSteps[globalIndex] = updatedStep;
|
|
} else {
|
|
state.executionSteps.push(updatedStep);
|
|
}
|
|
},
|
|
setActiveNodeId(state, action: PayloadAction<string | null>) {
|
|
state.activeNodeId = action.payload;
|
|
},
|
|
setStatus(state, action: PayloadAction<Status>) {
|
|
state.status = action.payload;
|
|
},
|
|
raiseError(
|
|
state,
|
|
action: PayloadAction<{
|
|
index: number;
|
|
message: string;
|
|
}>,
|
|
) {
|
|
const { index, message } = action.payload;
|
|
state.queries[index].error = message;
|
|
},
|
|
resetWorkflowPreview: (state) => {
|
|
state.queries = initialState.queries;
|
|
state.status = initialState.status;
|
|
state.executionSteps = initialState.executionSteps;
|
|
state.activeNodeId = initialState.activeNodeId;
|
|
handleWorkflowPreviewAbort();
|
|
},
|
|
clearExecutionSteps: (state) => {
|
|
state.executionSteps = [];
|
|
state.activeNodeId = null;
|
|
},
|
|
},
|
|
extraReducers(builder) {
|
|
builder
|
|
.addCase(fetchWorkflowPreviewAnswer.pending, (state) => {
|
|
state.status = 'loading';
|
|
state.executionSteps = [];
|
|
state.activeNodeId = null;
|
|
})
|
|
.addCase(fetchWorkflowPreviewAnswer.rejected, (state, action) => {
|
|
if (action.meta.aborted) {
|
|
state.status = 'idle';
|
|
return;
|
|
}
|
|
state.status = 'failed';
|
|
if (state.queries.length > 0) {
|
|
state.queries[state.queries.length - 1].error =
|
|
'Something went wrong';
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
interface RootStateWithWorkflowPreview {
|
|
workflowPreview: WorkflowPreviewState;
|
|
}
|
|
|
|
export const selectWorkflowPreviewQueries = (
|
|
state: RootStateWithWorkflowPreview,
|
|
) => state.workflowPreview.queries;
|
|
export const selectWorkflowPreviewStatus = (
|
|
state: RootStateWithWorkflowPreview,
|
|
) => state.workflowPreview.status;
|
|
export const selectWorkflowExecutionSteps = (
|
|
state: RootStateWithWorkflowPreview,
|
|
) => state.workflowPreview.executionSteps;
|
|
export const selectActiveNodeId = (state: RootStateWithWorkflowPreview) =>
|
|
state.workflowPreview.activeNodeId;
|
|
|
|
export const {
|
|
addQuery,
|
|
updateQuery,
|
|
resendQuery,
|
|
updateStreamingQuery,
|
|
updateThought,
|
|
updateStreamingSource,
|
|
updateToolCall,
|
|
updateExecutionStep,
|
|
setActiveNodeId,
|
|
setStatus,
|
|
raiseError,
|
|
resetWorkflowPreview,
|
|
clearExecutionSteps,
|
|
} = workflowPreviewSlice.actions;
|
|
|
|
export default workflowPreviewSlice.reducer;
|