diff --git a/common/constants.ts b/common/constants.ts
index fb891729..6cdbbd63 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -3,7 +3,12 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { TemplateNode, WORKFLOW_STATE } from './interfaces';
+import {
+ MODEL_ALGORITHM,
+ PRETRAINED_MODEL_FORMAT,
+ PretrainedSentenceTransformer,
+ WORKFLOW_STATE,
+} from './interfaces';
export const PLUGIN_ID = 'flow-framework';
@@ -52,6 +57,42 @@ export const SEARCH_MODELS_NODE_API_PATH = `${BASE_MODEL_NODE_API_PATH}/search`;
*/
export const CREATE_INGEST_PIPELINE_STEP_TYPE = 'create_ingest_pipeline';
export const CREATE_INDEX_STEP_TYPE = 'create_index';
+export const REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE =
+ 'register_local_pretrained_model';
+
+/**
+ * ML PLUGIN PRETRAINED MODELS
+ * (based off of https://opensearch.org/docs/latest/ml-commons-plugin/pretrained-models/#sentence-transformers)
+ */
+export const ROBERTA_SENTENCE_TRANSFORMER = {
+ name: 'huggingface/sentence-transformers/all-distilroberta-v1',
+ shortenedName: 'all-distilroberta-v1',
+ description: 'A sentence transformer from Hugging Face',
+ format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT,
+ algorithm: MODEL_ALGORITHM.TEXT_EMBEDDING,
+ version: '1.0.1',
+ vectorDimensions: 768,
+} as PretrainedSentenceTransformer;
+
+export const MPNET_SENTENCE_TRANSFORMER = {
+ name: 'huggingface/sentence-transformers/all-mpnet-base-v2',
+ shortenedName: 'all-mpnet-base-v2',
+ description: 'A sentence transformer from Hugging Face',
+ format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT,
+ algorithm: MODEL_ALGORITHM.TEXT_EMBEDDING,
+ version: '1.0.1',
+ vectorDimensions: 768,
+} as PretrainedSentenceTransformer;
+
+export const BERT_SENTENCE_TRANSFORMER = {
+ name: 'huggingface/sentence-transformers/msmarco-distilbert-base-tas-b',
+ shortenedName: 'msmarco-distilbert-base-tas-b',
+ description: 'A sentence transformer from Hugging Face',
+ format: PRETRAINED_MODEL_FORMAT.TORCH_SCRIPT,
+ algorithm: MODEL_ALGORITHM.TEXT_EMBEDDING,
+ version: '1.0.2',
+ vectorDimensions: 768,
+} as PretrainedSentenceTransformer;
/**
* MISCELLANEOUS
diff --git a/common/interfaces.ts b/common/interfaces.ts
index d9c6bc43..858965cf 100644
--- a/common/interfaces.ts
+++ b/common/interfaces.ts
@@ -82,6 +82,16 @@ export type CreateIndexNode = TemplateNode & {
};
};
+export type RegisterPretrainedModelNode = TemplateNode & {
+ user_inputs: {
+ name: string;
+ description: string;
+ model_format: string;
+ version: string;
+ deploy: boolean;
+ };
+};
+
export type TemplateEdge = {
source: string;
dest: string;
@@ -130,9 +140,83 @@ export enum USE_CASE {
/**
********** ML PLUGIN TYPES/INTERFACES **********
*/
+
+// Based off of https://github.com/opensearch-project/ml-commons/blob/main/common/src/main/java/org/opensearch/ml/common/model/MLModelState.java
+export enum MODEL_STATE {
+ REGISTERED = 'Registered',
+ REGISTERING = 'Registering',
+ DEPLOYING = 'Deploying',
+ DEPLOYED = 'Deployed',
+ PARTIALLY_DEPLOYED = 'Partially deployed',
+ UNDEPLOYED = 'Undeployed',
+ DEPLOY_FAILED = 'Deploy failed',
+}
+
+// Based off of https://github.com/opensearch-project/ml-commons/blob/main/common/src/main/java/org/opensearch/ml/common/FunctionName.java
+export enum MODEL_ALGORITHM {
+ LINEAR_REGRESSION = 'Linear regression',
+ KMEANS = 'K-means',
+ AD_LIBSVM = 'AD LIBSVM',
+ SAMPLE_ALGO = 'Sample algorithm',
+ LOCAL_SAMPLE_CALCULATOR = 'Local sample calculator',
+ FIT_RCF = 'Fit RCF',
+ BATCH_RCF = 'Batch RCF',
+ ANOMALY_LOCALIZATION = 'Anomaly localization',
+ RCF_SUMMARIZE = 'RCF summarize',
+ LOGISTIC_REGRESSION = 'Logistic regression',
+ TEXT_EMBEDDING = 'Text embedding',
+ METRICS_CORRELATION = 'Metrics correlation',
+ REMOTE = 'Remote',
+ SPARSE_ENCODING = 'Sparse encoding',
+ SPARSE_TOKENIZE = 'Sparse tokenize',
+ TEXT_SIMILARITY = 'Text similarity',
+ QUESTION_ANSWERING = 'Question answering',
+ AGENT = 'Agent',
+}
+
+export enum MODEL_CATEGORY {
+ DEPLOYED = 'Deployed',
+ PRETRAINED = 'Pretrained',
+}
+
+export enum PRETRAINED_MODEL_FORMAT {
+ TORCH_SCRIPT = 'TORCH_SCRIPT',
+}
+
+export type PretrainedModel = {
+ name: string;
+ shortenedName: string;
+ description: string;
+ format: PRETRAINED_MODEL_FORMAT;
+ algorithm: MODEL_ALGORITHM;
+ version: string;
+};
+
+export type PretrainedSentenceTransformer = PretrainedModel & {
+ vectorDimensions: number;
+};
+
+export type ModelConfig = {
+ modelType?: string;
+ embeddingDimension?: number;
+};
+
export type Model = {
id: string;
- algorithm: string;
+ name: string;
+ algorithm: MODEL_ALGORITHM;
+ state: MODEL_STATE;
+ modelConfig?: ModelConfig;
+};
+
+export type ModelDict = {
+ [modelId: string]: Model;
+};
+
+export type ModelFormValue = {
+ id: string;
+ category?: MODEL_CATEGORY;
+ algorithm?: MODEL_ALGORITHM;
};
/**
@@ -171,7 +255,3 @@ export enum WORKFLOW_RESOURCE_TYPE {
export type WorkflowDict = {
[workflowId: string]: Workflow;
};
-
-export type ModelDict = {
- [modelId: string]: Model;
-};
diff --git a/public/app.tsx b/public/app.tsx
index 2e14044d..fa80d217 100644
--- a/public/app.tsx
+++ b/public/app.tsx
@@ -62,12 +62,21 @@ export const FlowFrameworkDashboardsApp = (props: Props) => {
)}
/>
- {/* Defaulting to Workflows page */}
+ {/*
+ Defaulting to Workflows page. The pathname will need to be updated
+ to handle the redirection and get the router props consistent.
+ */}
) => (
-
- )}
+ render={(routeProps: RouteComponentProps) => {
+ if (props.history.location.pathname !== APP_PATH.WORKFLOWS) {
+ props.history.replace({
+ ...history,
+ pathname: APP_PATH.WORKFLOWS,
+ });
+ }
+ return ;
+ }}
/>
diff --git a/public/component_types/interfaces.ts b/public/component_types/interfaces.ts
index 0056d700..055f44ff 100644
--- a/public/component_types/interfaces.ts
+++ b/public/component_types/interfaces.ts
@@ -10,7 +10,7 @@ import { COMPONENT_CATEGORY, COMPONENT_CLASS } from '../utils';
/**
* ************ Types *************************
*/
-export type FieldType = 'string' | 'json' | 'select';
+export type FieldType = 'string' | 'json' | 'select' | 'model';
export type SelectType = 'model';
export type FieldValue = string | {};
export type ComponentFormValues = FormikValues;
diff --git a/public/component_types/transformer/text_embedding_transformer.ts b/public/component_types/transformer/text_embedding_transformer.ts
index c856381e..affb996c 100644
--- a/public/component_types/transformer/text_embedding_transformer.ts
+++ b/public/component_types/transformer/text_embedding_transformer.ts
@@ -19,11 +19,10 @@ export class TextEmbeddingTransformer extends MLTransformer {
this.inputs = [];
this.createFields = [
{
- label: 'Model ID',
- id: 'modelId',
- type: 'select',
- selectType: 'model',
- helpText: 'The deployed text embedding model to use for embedding.',
+ label: 'Text Embedding Model',
+ id: 'model',
+ type: 'model',
+ helpText: 'A text embedding model for embedding text.',
helpLink:
'https://opensearch.org/docs/latest/ml-commons-plugin/integrating-ml-models/#choosing-a-model',
},
@@ -36,7 +35,6 @@ export class TextEmbeddingTransformer extends MLTransformer {
helpLink:
'https://opensearch.org/docs/latest/ingest-pipelines/processors/text-embedding/',
},
-
{
label: 'Vector Field',
id: 'vectorField',
diff --git a/public/pages/workflow_detail/component_details/component_details.tsx b/public/pages/workflow_detail/component_details/component_details.tsx
index 41ed85d0..1de35468 100644
--- a/public/pages/workflow_detail/component_details/component_details.tsx
+++ b/public/pages/workflow_detail/component_details/component_details.tsx
@@ -28,9 +28,12 @@ interface ComponentDetailsProps {
export function ComponentDetails(props: ComponentDetailsProps) {
return (
- {props.isDeprovisionable ? (
+ {/* TODO: determine if we need this view if we want the workspace to remain
+ readonly once provisioned */}
+ {/* {props.isDeprovisionable ? (
- ) : props.selectedComponent ? (
+ ) : */}
+ {props.selectedComponent ? (
{props.selectedComponent.data.label || ''}
+
+ {props.selectedComponent.data.description}
+
+
+
+
+ );
+ break;
+ }
case 'json': {
el = (
diff --git a/public/pages/workflow_detail/component_details/input_fields/index.ts b/public/pages/workflow_detail/component_details/input_fields/index.ts
index e2edf8bd..7d0561f5 100644
--- a/public/pages/workflow_detail/component_details/input_fields/index.ts
+++ b/public/pages/workflow_detail/component_details/input_fields/index.ts
@@ -6,3 +6,4 @@
export { TextField } from './text_field';
export { JsonField } from './json_field';
export { SelectField } from './select_field';
+export { ModelField } from './model_field';
diff --git a/public/pages/workflow_detail/component_details/input_fields/model_field.tsx b/public/pages/workflow_detail/component_details/input_fields/model_field.tsx
new file mode 100644
index 00000000..26ba8e37
--- /dev/null
+++ b/public/pages/workflow_detail/component_details/input_fields/model_field.tsx
@@ -0,0 +1,215 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { Field, FieldProps, useFormikContext } from 'formik';
+import {
+ EuiFormRow,
+ EuiLink,
+ EuiRadioGroup,
+ EuiRadioGroupOption,
+ EuiSpacer,
+ EuiSuperSelect,
+ EuiSuperSelectOption,
+ EuiText,
+} from '@elastic/eui';
+import {
+ BERT_SENTENCE_TRANSFORMER,
+ IComponentField,
+ MODEL_STATE,
+ ROBERTA_SENTENCE_TRANSFORMER,
+ WorkspaceFormValues,
+ isFieldInvalid,
+ ModelFormValue,
+ MODEL_CATEGORY,
+ MPNET_SENTENCE_TRANSFORMER,
+} from '../../../../../common';
+import { AppState } from '../../../../store';
+
+interface ModelFieldProps {
+ field: IComponentField;
+ componentId: string;
+ onFormChange: () => void;
+}
+
+type ModelItem = ModelFormValue & {
+ name: string;
+};
+
+// TODO: there is no concrete UX for model selection and model provisioning. This component is TBD
+// and simply provides the ability to select existing models, or deploy some pretrained ones,
+// and persist all of this in form state.
+/**
+ * A specific field for selecting existing deployed models, or available pretrained models.
+ */
+export function ModelField(props: ModelFieldProps) {
+ // Initial store is fetched when loading base page. We don't
+ // re-fetch here as it could overload client-side if user clicks back and forth /
+ // keeps re-rendering this component (and subsequently re-fetching data) as they're building flows
+ const models = useSelector((state: AppState) => state.models.models);
+
+ const formField = `${props.componentId}.${props.field.id}`;
+ const { errors, touched } = useFormikContext();
+
+ // Deployed models state
+ const [deployedModels, setDeployedModels] = useState([]);
+ const [pretrainedModels, setPretrainedModels] = useState([]);
+ const [selectableModels, setSelectableModels] = useState([]);
+
+ // Radio options state
+ const radioOptions = [
+ {
+ id: MODEL_CATEGORY.DEPLOYED,
+ label: 'Existing deployed models',
+ },
+ {
+ id: MODEL_CATEGORY.PRETRAINED,
+ label: 'Pretrained models',
+ },
+ ] as EuiRadioGroupOption[];
+ const [selectedRadioId, setSelectedRadioId] = useState<
+ MODEL_CATEGORY | undefined
+ >(undefined);
+
+ // Initialize available deployed models
+ useEffect(() => {
+ if (models) {
+ const modelItems = [] as ModelItem[];
+ Object.keys(models).forEach((modelId) => {
+ if (models[modelId].state === MODEL_STATE.DEPLOYED) {
+ modelItems.push({
+ id: modelId,
+ name: models[modelId].name,
+ category: MODEL_CATEGORY.DEPLOYED,
+ algorithm: models[modelId].algorithm,
+ } as ModelItem);
+ }
+ });
+ setDeployedModels(modelItems);
+ }
+ }, [models]);
+
+ // Initialize available pretrained models
+ useEffect(() => {
+ const modelItems = [
+ {
+ id: ROBERTA_SENTENCE_TRANSFORMER.name,
+ name: ROBERTA_SENTENCE_TRANSFORMER.shortenedName,
+ category: MODEL_CATEGORY.PRETRAINED,
+ algorithm: ROBERTA_SENTENCE_TRANSFORMER.algorithm,
+ },
+ {
+ id: MPNET_SENTENCE_TRANSFORMER.name,
+ name: MPNET_SENTENCE_TRANSFORMER.shortenedName,
+ category: MODEL_CATEGORY.PRETRAINED,
+ algorithm: MPNET_SENTENCE_TRANSFORMER.algorithm,
+ },
+ {
+ id: BERT_SENTENCE_TRANSFORMER.name,
+ name: BERT_SENTENCE_TRANSFORMER.shortenedName,
+ category: MODEL_CATEGORY.PRETRAINED,
+ algorithm: BERT_SENTENCE_TRANSFORMER.algorithm,
+ },
+ ];
+ setPretrainedModels(modelItems);
+ }, []);
+
+ // Update the valid available models when the radio selection changes.
+ // e.g., only show deployed models when 'deployed' button is selected
+ useEffect(() => {
+ if (selectedRadioId !== undefined) {
+ if (selectedRadioId === MODEL_CATEGORY.DEPLOYED) {
+ setSelectableModels(deployedModels);
+ } else {
+ setSelectableModels(pretrainedModels);
+ }
+ }
+ }, [selectedRadioId, deployedModels, pretrainedModels]);
+
+ return (
+
+ {({ field, form }: FieldProps) => {
+ // a hook to update the model category and trigger reloading
+ // of valid models to select from
+ useEffect(() => {
+ setSelectedRadioId(field.value.category);
+ }, [field.value.category]);
+ return (
+
+
+ Learn more
+
+
+ ) : undefined
+ }
+ helpText={props.field.helpText || undefined}
+ >
+ <>
+ {
+ // if user selects a new category:
+ // 1. clear the saved ID
+ // 2. update the field category
+ form.setFieldValue(formField, {
+ id: '',
+ category: radioId,
+ } as ModelFormValue);
+ props.onFormChange();
+ }}
+ >
+
+
+ ({
+ value: option.id,
+ inputDisplay: (
+ <>
+ {option.name}
+ >
+ ),
+ dropdownDisplay: (
+ <>
+ {option.name}
+
+ {option.category}
+
+
+ {option.algorithm}
+
+ >
+ ),
+ disabled: false,
+ } as EuiSuperSelectOption)
+ )}
+ valueOfSelected={field.value.id || ''}
+ onChange={(option: string) => {
+ form.setFieldValue(formField, {
+ id: option,
+ category: selectedRadioId,
+ } as ModelFormValue);
+ props.onFormChange();
+ }}
+ isInvalid={isFieldInvalid(
+ props.componentId,
+ props.field.id,
+ errors,
+ touched
+ )}
+ />
+ >
+
+ );
+ }}
+
+ );
+}
diff --git a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx
index 709fe18c..38017e04 100644
--- a/public/pages/workflow_detail/component_details/input_fields/select_field.tsx
+++ b/public/pages/workflow_detail/component_details/input_fields/select_field.tsx
@@ -19,7 +19,6 @@ import {
getInitialValue,
isFieldInvalid,
} from '../../../../../common';
-import { AppState } from '../../../../store';
interface SelectFieldProps {
field: IComponentField;
@@ -32,21 +31,15 @@ interface SelectFieldProps {
* options.
*/
export function SelectField(props: SelectFieldProps) {
- // Redux store state
- // Initial store is fetched when loading base page. We don't
- // re-fetch here as it could overload client-side if user clicks back and forth /
- // keeps re-rendering this component (and subsequently re-fetching data) as they're building flows
- const models = useSelector((state: AppState) => state.models.models);
-
// Options state
const [options, setOptions] = useState([]);
// Populate options depending on the select type
useEffect(() => {
- if (props.field.selectType === 'model' && models) {
- setOptions(Object.keys(models));
+ // TODO: figure out how we want to utilize select types to customize the options
+ if (props.field.selectType === 'model') {
}
- }, [models]);
+ }, []);
const formField = `${props.componentId}.${props.field.id}`;
const { errors, touched } = useFormikContext();
diff --git a/public/pages/workflow_detail/resources/resource_list.tsx b/public/pages/workflow_detail/resources/resource_list.tsx
index 0acb0f9f..72a10f1f 100644
--- a/public/pages/workflow_detail/resources/resource_list.tsx
+++ b/public/pages/workflow_detail/resources/resource_list.tsx
@@ -23,10 +23,16 @@ interface ResourceListProps {
export function ResourceList(props: ResourceListProps) {
const [allResources, setAllResources] = useState([]);
- // Hook to initialize all resources
+ // Hook to initialize all resources. Reduce to unique IDs, since
+ // the backend resources may include the same resource multiple times
+ // (e.g., register and deploy steps persist the same model ID resource)
useEffect(() => {
if (props.workflow?.resourcesCreated) {
- setAllResources(props.workflow.resourcesCreated);
+ const resourcesMap = {} as { [id: string]: WorkflowResource };
+ props.workflow.resourcesCreated.forEach((resource) => {
+ resourcesMap[resource.id] = resource;
+ });
+ setAllResources(Object.values(resourcesMap));
}
}, [props.workflow?.resourcesCreated]);
diff --git a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts
index 3e9ce61f..399129d8 100644
--- a/public/pages/workflow_detail/utils/workflow_to_template_utils.ts
+++ b/public/pages/workflow_detail/utils/workflow_to_template_utils.ts
@@ -20,6 +20,15 @@ import {
CreateIndexNode,
TemplateFlow,
TemplateEdge,
+ ModelFormValue,
+ MODEL_CATEGORY,
+ RegisterPretrainedModelNode,
+ PretrainedSentenceTransformer,
+ ROBERTA_SENTENCE_TRANSFORMER,
+ MPNET_SENTENCE_TRANSFORMER,
+ BERT_SENTENCE_TRANSFORMER,
+ REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE,
+ generateId,
} from '../../../../common';
/**
@@ -68,13 +77,13 @@ function toProvisionTemplateFlow(
edges: ReactFlowEdge[]
): TemplateFlow {
const prevNodes = [] as ReactFlowComponent[];
- const templateNodes = [] as TemplateNode[];
+ const finalTemplateNodes = [] as TemplateNode[];
const templateEdges = [] as TemplateEdge[];
nodes.forEach((node) => {
- const templateNode = toTemplateNode(node, prevNodes, edges);
+ const templateNodes = toTemplateNodes(node, prevNodes, edges);
// it may be undefined if the node is not convertible for some reason
- if (templateNode) {
- templateNodes.push(templateNode);
+ if (templateNodes) {
+ finalTemplateNodes.push(...templateNodes);
prevNodes.push(node);
}
});
@@ -84,20 +93,20 @@ function toProvisionTemplateFlow(
});
return {
- nodes: templateNodes,
+ nodes: finalTemplateNodes,
edges: templateEdges,
};
}
-function toTemplateNode(
+function toTemplateNodes(
flowNode: ReactFlowComponent,
prevNodes: ReactFlowComponent[],
edges: ReactFlowEdge[]
-): TemplateNode | undefined {
+): TemplateNode[] | undefined {
if (flowNode.data.baseClasses?.includes(COMPONENT_CLASS.ML_TRANSFORMER)) {
- return toIngestPipelineNode(flowNode);
+ return transformerToTemplateNodes(flowNode);
} else if (flowNode.data.baseClasses?.includes(COMPONENT_CLASS.INDEXER)) {
- return toIndexerNode(flowNode, prevNodes, edges);
+ return [indexerToTemplateNode(flowNode, prevNodes, edges)];
}
}
@@ -110,9 +119,11 @@ function toTemplateEdge(flowEdge: ReactFlowEdge): TemplateEdge {
// General fn to process all ML transform nodes. Convert into a final
// ingest pipeline with a processor specific to the final class of the node.
-function toIngestPipelineNode(
+// Optionally prepend a register pretrained model step if the selected model
+// is a pretrained and undeployed one.
+function transformerToTemplateNodes(
flowNode: ReactFlowComponent
-): CreateIngestPipelineNode {
+): TemplateNode[] {
// TODO a few improvements to make here:
// 1. Consideration of multiple ingest processors and how to collect them all, and finally create
// a single ingest pipeline with all of them, in the same order as done on the UI
@@ -120,17 +131,54 @@ function toIngestPipelineNode(
switch (flowNode.data.type) {
case COMPONENT_CLASS.TEXT_EMBEDDING_TRANSFORMER:
default: {
- const { modelId, inputField, vectorField } = componentDataToFormik(
+ const { model, inputField, vectorField } = componentDataToFormik(
flowNode.data
- );
+ ) as {
+ model: ModelFormValue;
+ inputField: string;
+ vectorField: string;
+ };
+ const modelId = model.id;
+ const ingestPipelineName = generateId('ingest_pipeline');
- return {
+ let registerModelStep = undefined as
+ | RegisterPretrainedModelNode
+ | undefined;
+ if (model.category === MODEL_CATEGORY.PRETRAINED) {
+ const pretrainedModel = [
+ ROBERTA_SENTENCE_TRANSFORMER,
+ MPNET_SENTENCE_TRANSFORMER,
+ BERT_SENTENCE_TRANSFORMER,
+ ].find(
+ // the model ID in the form will be the unique name of the pretrained model
+ (model) => model.name === modelId
+ ) as PretrainedSentenceTransformer;
+ registerModelStep = {
+ id: REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE,
+ type: REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE,
+ user_inputs: {
+ name: pretrainedModel.name,
+ description: pretrainedModel.description,
+ model_format: pretrainedModel.format,
+ version: pretrainedModel.version,
+ deploy: true,
+ },
+ } as RegisterPretrainedModelNode;
+ }
+
+ // The model ID depends on if we are consuming it from a previous pretrained model step,
+ // or directly from the user
+ const finalModelId =
+ registerModelStep !== undefined
+ ? `\${{${REGISTER_LOCAL_PRETRAINED_MODEL_STEP_TYPE}.model_id}}`
+ : modelId;
+
+ const createIngestPipelineStep = {
id: flowNode.data.id,
type: CREATE_INGEST_PIPELINE_STEP_TYPE,
user_inputs: {
- // TODO: expose as customizable
- pipeline_id: 'test-pipeline',
- model_id: modelId,
+ pipeline_id: ingestPipelineName,
+ model_id: finalModelId,
input_field: inputField,
output_field: vectorField,
configurations: {
@@ -138,7 +186,7 @@ function toIngestPipelineNode(
processors: [
{
text_embedding: {
- model_id: modelId,
+ model_id: finalModelId,
field_map: {
[inputField]: vectorField,
},
@@ -147,13 +195,17 @@ function toIngestPipelineNode(
],
},
},
- };
+ } as CreateIngestPipelineNode;
+
+ return registerModelStep !== undefined
+ ? [registerModelStep, createIngestPipelineStep]
+ : [createIngestPipelineStep];
}
}
}
// General fn to convert an indexer node to a final CreateIndexNode template node.
-function toIndexerNode(
+function indexerToTemplateNode(
flowNode: ReactFlowComponent,
prevNodes: ReactFlowComponent[],
edges: ReactFlowEdge[]
@@ -191,6 +243,8 @@ function toIndexerNode(
properties: {
[vectorField]: {
type: 'knn_vector',
+ // TODO: remove hardcoding, fetch from the selected model
+ // (existing or from pretrained configuration)
dimension: 768,
method: {
engine: 'lucene',
diff --git a/public/pages/workflow_detail/workspace/resizable_workspace.tsx b/public/pages/workflow_detail/workspace/resizable_workspace.tsx
index 37a37599..90cba2fb 100644
--- a/public/pages/workflow_detail/workspace/resizable_workspace.tsx
+++ b/public/pages/workflow_detail/workspace/resizable_workspace.tsx
@@ -33,6 +33,7 @@ import {
processNodes,
reduceToTemplate,
ReactFlowEdge,
+ APP_PATH,
} from '../../../../common';
import { validateWorkspaceFlow, toTemplateFlows } from '../utils';
import {
@@ -116,6 +117,8 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
props.workflow !== undefined &&
!props.isNewWorkflow &&
props.workflow?.state !== WORKFLOW_STATE.NOT_STARTED;
+ // TODO: maybe remove this field. It depends on final UX if we want the
+ // workspace to be readonly once provisioned or not.
const readonly = props.workflow === undefined || isDeprovisionable;
// Loading state
@@ -168,7 +171,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
props.workflow && !props.workflow?.ui_metadata?.workspace_flow;
const missingCachedWorkflow = props.isNewWorkflow && !props.workflow;
if (missingUiFlow || missingCachedWorkflow) {
- history.replace('/workflows');
+ history.replace(APP_PATH.WORKFLOWS);
if (missingCachedWorkflow) {
getCore().notifications.toasts.addWarning('No workflow found');
} else {
@@ -320,6 +323,17 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
components.
)}
+ {isDeprovisionable && isDirty && (
+
+ Changes cannot be saved until the flow has first been
+ deprovisioned.
+
+ )}
{
const { workflow } = result;
- history.replace(`/workflows/${workflow.id}`);
+ history.replace(
+ `${APP_PATH.WORKFLOWS}/${workflow.id}`
+ );
history.go(0);
})
.catch((error: any) => {
@@ -455,7 +471,7 @@ export function ResizableWorkspace(props: ResizableWorkspaceProps) {
diff --git a/public/pages/workflows/new_workflow/use_case.tsx b/public/pages/workflows/new_workflow/use_case.tsx
index 111c4f7c..a6eceacf 100644
--- a/public/pages/workflows/new_workflow/use_case.tsx
+++ b/public/pages/workflows/new_workflow/use_case.tsx
@@ -13,7 +13,7 @@ import {
EuiHorizontalRule,
EuiButton,
} from '@elastic/eui';
-import { NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common';
+import { APP_PATH, NEW_WORKFLOW_ID_URL, PLUGIN_ID } from '../../../../common';
interface UseCaseProps {
title: string;
@@ -44,7 +44,7 @@ export function UseCase(props: UseCaseProps) {
disabled={false}
isLoading={false}
onClick={props.onClick}
- href={`${PLUGIN_ID}#/workflows/${NEW_WORKFLOW_ID_URL}`}
+ href={`${PLUGIN_ID}#${APP_PATH.WORKFLOWS}/${NEW_WORKFLOW_ID_URL}`}
>
Go
diff --git a/public/utils/constants.ts b/public/utils/constants.ts
index 0f3b75f6..74dd67b7 100644
--- a/public/utils/constants.ts
+++ b/public/utils/constants.ts
@@ -15,7 +15,7 @@ export enum APP_PATH {
}
export const BREADCRUMBS = Object.freeze({
- FLOW_FRAMEWORK: { text: 'Flow Framework', href: '#/' },
+ FLOW_FRAMEWORK: { text: 'Flow Framework' },
WORKFLOWS: { text: 'Workflows', href: `#${APP_PATH.WORKFLOWS}` },
});
diff --git a/public/utils/utils.ts b/public/utils/utils.ts
index 3f9a7b8b..76b6715e 100644
--- a/public/utils/utils.ts
+++ b/public/utils/utils.ts
@@ -18,6 +18,7 @@ import {
ReactFlowComponent,
Workflow,
WorkflowTemplate,
+ ModelFormValue,
} from '../../common';
// Append 16 random characters
@@ -82,6 +83,7 @@ export function reduceToTemplate(workflow: Workflow): WorkflowTemplate {
lastUpdated,
lastLaunched,
state,
+ resourcesCreated,
...workflowTemplate
} = workflow;
return workflowTemplate;
@@ -96,6 +98,13 @@ export function getInitialValue(fieldType: FieldType): FieldValue {
case 'select': {
return '';
}
+ case 'model': {
+ return {
+ id: '',
+ category: undefined,
+ algorithm: undefined,
+ } as ModelFormValue;
+ }
case 'json': {
return {};
}
@@ -162,6 +171,13 @@ function getFieldSchema(field: IComponentField): Schema {
baseSchema = yup.string().min(1, 'Too short').max(70, 'Too long');
break;
}
+ case 'model': {
+ baseSchema = yup.object().shape({
+ id: yup.string().min(1, 'Too short').max(70, 'Too long').required(),
+ category: yup.string().required(),
+ });
+ break;
+ }
case 'json': {
baseSchema = yup.object().json();
break;
diff --git a/server/routes/helpers.ts b/server/routes/helpers.ts
index b4d39991..afb404f7 100644
--- a/server/routes/helpers.ts
+++ b/server/routes/helpers.ts
@@ -6,6 +6,8 @@
import {
DEFAULT_NEW_WORKFLOW_STATE_TYPE,
INDEX_NOT_FOUND_EXCEPTION,
+ MODEL_ALGORITHM,
+ MODEL_STATE,
Model,
ModelDict,
WORKFLOW_RESOURCE_TYPE,
@@ -85,13 +87,25 @@ export function getWorkflowsFromResponses(
export function getModelsFromResponses(modelHits: any[]): ModelDict {
const modelDict = {} as ModelDict;
modelHits.forEach((modelHit: any) => {
- const modelId = modelHit._source?.model_id;
- // in case of schema changes from ML plugin, this may crash. That is ok, as the error
- // produced will help expose the root cause
- modelDict[modelId] = {
- id: modelId,
- algorithm: modelHit._source?.algorithm,
- } as Model;
+ // search model API returns hits for each deployed model chunk. ignore these hits
+ if (modelHit._source.chunk_number === undefined) {
+ const modelId = modelHit._id;
+ // in case of schema changes from ML plugin, this may crash. That is ok, as the error
+ // produced will help expose the root cause
+ modelDict[modelId] = {
+ id: modelId,
+ name: modelHit._source?.name,
+ // @ts-ignore
+ algorithm: MODEL_ALGORITHM[modelHit._source?.algorithm],
+ // @ts-ignore
+ state: MODEL_STATE[modelHit._source?.model_state],
+ modelConfig: {
+ modelType: modelHit._source?.model_config?.model_type,
+ embeddingDimension:
+ modelHit._source?.model_config?.embedding_dimension,
+ },
+ } as Model;
+ }
});
return modelDict;
}