Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add server-side workflow template library #112

Merged
merged 4 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const SEARCH_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/se
export const GET_WORKFLOW_STATE_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/state`;
export const CREATE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/create`;
export const DELETE_WORKFLOW_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/delete`;
export const GET_PRESET_WORKFLOWS_NODE_API_PATH = `${BASE_WORKFLOW_NODE_API_PATH}/presets`;

/**
* MISCELLANEOUS
Expand Down
1 change: 1 addition & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

export * from './constants';
export * from './interfaces';
export * from './utils';
export * from '../public/component_types';
export * from '../public/utils';
29 changes: 14 additions & 15 deletions common/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ export type WorkspaceFlowState = {

export type TemplateNode = {
id: string;
inputs: {};
type: string;
previous_node_inputs?: Map<string, any>;
user_inputs?: Map<string, any>;
};

export type TemplateEdge = {
Expand All @@ -49,32 +51,30 @@ export type TemplateEdge = {
};

export type TemplateFlow = {
userParams: {};
user_params?: Map<string, any>;
nodes: TemplateNode[];
edges: TemplateEdge[];
edges?: TemplateEdge[];
};

export type TemplateFlows = {
provision: TemplateFlow;
ingest: TemplateFlow;
query: TemplateFlow;
};

export type UseCaseTemplate = {
type: string;
// A stateless template of a workflow
export type WorkflowTemplate = {
name: string;
description: string;
userInputs: {};
use_case: USE_CASE;
// TODO: finalize on version type when that is implemented
// https://github.com/opensearch-project/flow-framework/issues/526
version: any;
workflows: TemplateFlows;
};

export type Workflow = {
// An instance of a workflow based on a workflow template
export type Workflow = WorkflowTemplate & {
// won't exist until created in backend
id?: string;
name: string;
useCase: string;
template: UseCaseTemplate;
description?: string;
// ReactFlow state may not exist if a workflow is created via API/backend-only.
workspaceFlowState?: WorkspaceFlowState;
// won't exist until created in backend
Expand All @@ -86,8 +86,7 @@ export type Workflow = {
};

export enum USE_CASE {
SEMANTIC_SEARCH = 'semantic_search',
CUSTOM = 'custom',
PROVISION = 'PROVISION',
}

/**
Expand Down
93 changes: 93 additions & 0 deletions common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
WorkspaceFlowState,
ReactFlowComponent,
initComponentData,
TextEmbeddingTransformer,
KnnIndexer,
generateId,
ReactFlowEdge,
TemplateFlows,
WorkflowTemplate,
} from './';

// TODO: implement this and remove hardcoded return values
/**
* Converts a ReactFlow workspace flow to a backend-compatible set of ingest and/or search sub-workflows,
* along with a provision sub-workflow if resources are to be created.
*/
export function toTemplateFlows(
workspaceFlow: WorkspaceFlowState
): TemplateFlows {
return {
provision: {
user_params: {} as Map<string, any>,
nodes: [],
edges: [],
},
};
}

// TODO: implement this and remove hardcoded return values
/**
* Converts a backend set of provision/ingest/search sub-workflows into a UI-compatible set of
* ReactFlow nodes and edges
*/
export function toWorkspaceFlow(
templateFlows: TemplateFlows
): WorkspaceFlowState {
const id1 = generateId('text_embedding_processor');
const id2 = generateId('text_embedding_processor');
const id3 = generateId('knn_index');
const dummyNodes = [
{
id: id1,
position: { x: 0, y: 500 },
data: initComponentData(new TextEmbeddingTransformer().toObj(), id1),
type: 'customComponent',
},
{
id: id2,
position: { x: 0, y: 200 },
data: initComponentData(new TextEmbeddingTransformer().toObj(), id2),
type: 'customComponent',
},
{
id: id3,
position: { x: 500, y: 500 },
data: initComponentData(new KnnIndexer().toObj(), id3),
type: 'customComponent',
},
] as ReactFlowComponent[];

return {
nodes: dummyNodes,
edges: [] as ReactFlowEdge[],
};
}

// TODO: implement this
/**
* Validates the UI workflow state.
* Note we don't have to validate connections since that is done via input/output handlers.
*/
export function validateWorkspaceFlow(
workspaceFlow: WorkspaceFlowState
): boolean {
return true;
}

// TODO: implement this
/**
* Validates the backend template. May be used when parsing persisted templates on server-side,
* or when importing/exporting on the UI.
*/
export function validateWorkflowTemplate(
workflowTemplate: WorkflowTemplate
): boolean {
return true;
}
44 changes: 4 additions & 40 deletions public/pages/workflow_detail/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import {
WorkspaceFlowState,
UseCaseTemplate,
Workflow,
USE_CASE,
ReactFlowComponent,
toTemplateFlows,
validateWorkspaceFlow,
} from '../../../../common';

export function saveWorkflow(workflow: Workflow, rfInstance: any): void {
Expand All @@ -19,12 +19,12 @@ export function saveWorkflow(workflow: Workflow, rfInstance: any): void {
nodes: processNodes(curFlowState.nodes),
};

const isValid = validateFlowState(curFlowState);
const isValid = validateWorkspaceFlow(curFlowState);
if (isValid) {
const updatedWorkflow = {
...workflow,
workspaceFlowState: curFlowState,
template: generateUseCaseTemplate(curFlowState),
workflows: toTemplateFlows(curFlowState),
} as Workflow;
if (workflow.id) {
// TODO: implement connection to update workflow API
Expand All @@ -36,42 +36,6 @@ export function saveWorkflow(workflow: Workflow, rfInstance: any): void {
}
}

// TODO: implement this. Need more info on UX side to finalize what we need
// to persist, what validation to do, etc.
// Note we don't have to validate connections since that is done via input/output handlers.
function validateFlowState(flowState: WorkspaceFlowState): boolean {
return true;
}

// TODO: implement this
function generateUseCaseTemplate(
flowState: WorkspaceFlowState
): UseCaseTemplate {
return {
name: 'example-name',
description: 'example description',
type: USE_CASE.SEMANTIC_SEARCH,
userInputs: {},
workflows: {
provision: {
userParams: {},
nodes: [],
edges: [],
},
ingest: {
userParams: {},
nodes: [],
edges: [],
},
query: {
userParams: {},
nodes: [],
edges: [],
},
},
} as UseCaseTemplate;
}

// Process the raw ReactFlow nodes to only persist the fields we need
function processNodes(nodes: ReactFlowComponent[]): ReactFlowComponent[] {
return nodes
Expand Down
11 changes: 9 additions & 2 deletions public/pages/workflow_detail/workflow_detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,15 @@ export function WorkflowDetail(props: WorkflowDetailProps) {
// TODO: can optimize to only fetch a single workflow
dispatch(searchWorkflows({ query: { match_all: {} } }));
}
window.onbeforeunload = (e) =>
isDirty || isNewWorkflow ? true : undefined;

// TODO: below has the following issue:
// 1. user starts to create new unsaved workflow changes
// 2. user navigates to other parts of the plugin without refreshing - no warning happens
// 3. user refreshes at any later time: if isDirty is still true, shows browser warning
// tune to only handle the check if still on the workflow details page, or consider adding a check / warning
// if navigating away from the details page without refreshing (where it is currently not being triggered)
// window.onbeforeunload = (e) =>
// isDirty || isNewWorkflow ? true : undefined;
}, []);

const tabs = [
Expand Down
19 changes: 11 additions & 8 deletions public/pages/workflow_detail/workspace/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import {
IComponentData,
ReactFlowComponent,
Workflow,
toWorkspaceFlow,
} from '../../../../common';
import { generateId, initComponentData } from '../../../utils';
import { getCore } from '../../../services';
import { WorkspaceComponent } from '../workspace_component';
import { DeletableEdge } from '../workspace_edge';

Expand Down Expand Up @@ -119,16 +119,19 @@ export function Workspace(props: WorkspaceProps) {
// Initialization. Set the nodes and edges to an existing workflow,
// if applicable.
useEffect(() => {
const workflow = props.workflow;
const workflow = { ...props.workflow };
if (workflow) {
if (workflow.workspaceFlowState) {
setNodes(workflow.workspaceFlowState.nodes);
setEdges(workflow.workspaceFlowState.edges);
} else {
getCore().notifications.toasts.addWarning(
`There is no configured UI flow for workflow: ${workflow.name}`
if (!workflow.workspaceFlowState) {
// No existing workspace state. This could be due to it being a backend-only-created
// workflow, or a new, unsaved workflow
// @ts-ignore
workflow.workspaceFlowState = toWorkspaceFlow(workflow.workflows);
console.debug(
`There is no saved UI flow for workflow: ${workflow.name}. Generating a default one.`
);
}
setNodes(workflow.workspaceFlowState.nodes);
setEdges(workflow.workspaceFlowState.edges);
}
}, [props.workflow]);

Expand Down
Loading
Loading