Add inference endpoints management page

This commit is contained in:
Saikat Sarkar 2024-05-13 15:06:09 -06:00
parent 53bdba31a0
commit 8c6f6d8c13
14 changed files with 4865 additions and 0 deletions

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { AddEmptyPrompt } from '../../../shared/add_empty_prompt_for_inference_endpoint/add_empty_prompt';
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
interface EmptyPromptPageProps {
addEndpointLabel: string;
breadcrumbs: string[];
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const EmptyPromptPage: React.FC<EmptyPromptPageProps> = ({
addEndpointLabel,
breadcrumbs,
setIsInferenceFlyoutVisible,
}) => (
<EnterpriseSearchContentPageTemplate
pageChrome={breadcrumbs}
pageViewTelemetry="Inference Endpoints"
isLoading={false}
>
<AddEmptyPrompt
addEndpointLabel={addEndpointLabel}
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
/>
</EnterpriseSearchContentPageTemplate>
);

View file

@ -0,0 +1,165 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { useValues } from 'kea';
import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { InferenceFlyoutWrapper } from '@kbn/inference_integration_flyout/components/inference_flyout_wrapper';
import { ModelConfig } from '@kbn/inference_integration_flyout/types';
import { extractErrorProperties } from '@kbn/ml-error-utils';
import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models';
import { SUPPORTED_PYTORCH_TASKS, TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils';
import { docLinks } from '../../../shared/doc_links';
import { KibanaLogic } from '../../../shared/kibana';
import { EmptyPromptPage } from './empty_prompt_page';
import { TabularPage } from './tabular_page';
const inferenceEndpoints = [];
const inferenceEndpointsBreadcrumbs = [
i18n.translate('xpack.enterpriseSearch.content.inferenceEndpoints.breadcrumb', {
defaultMessage: 'Inference Endpoints',
}),
];
const addEndpointLabel = i18n.translate(
'xpack.enterpriseSearch.content.inferenceEndpoints.newInferenceEndpointButtonLabel',
{
defaultMessage: 'Add endpoint',
}
);
export const InferenceEndpoints: React.FC = () => {
const { ml } = useValues(KibanaLogic);
const [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] = useState<boolean>(false);
const [inferenceAddError, setInferenceAddError] = useState<string | undefined>(undefined);
const [isCreateInferenceApiLoading, setIsCreateInferenceApiLoading] = useState(false);
const [availableTrainedModels, setAvailableTrainedModels] = useState<
TrainedModelConfigResponse[]
>([]);
const [inferenceEndpointError, setInferenceEndpointError] = useState<string | undefined>(
undefined
);
const onInferenceEndpointChange = async () => {};
const onSaveInferenceCallback = useCallback(
async (inferenceId: string, taskType: InferenceTaskType, modelConfig: ModelConfig) => {
setIsCreateInferenceApiLoading(true);
try {
await ml?.mlApi?.inferenceModels?.createInferenceEndpoint(
inferenceId,
taskType,
modelConfig
);
setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
setIsCreateInferenceApiLoading(false);
setInferenceAddError(undefined);
} catch (error) {
const errorObj = extractErrorProperties(error);
setInferenceAddError(errorObj.message);
setIsCreateInferenceApiLoading(false);
}
},
[isInferenceFlyoutVisible, ml]
);
const onFlyoutClose = useCallback(() => {
setInferenceAddError(undefined);
setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
}, [isInferenceFlyoutVisible]);
useEffect(() => {
const fetchAvailableTrainedModels = async () => {
setAvailableTrainedModels((await ml?.mlApi?.trainedModels?.getTrainedModels()) ?? []);
};
fetchAvailableTrainedModels();
}, [ml]);
const trainedModels = useMemo(() => {
const availableTrainedModelsList = availableTrainedModels
.filter(
(model: TrainedModelConfigResponse) =>
model.model_type === TRAINED_MODEL_TYPE.PYTORCH &&
(model?.inference_config
? Object.keys(model.inference_config).includes(SUPPORTED_PYTORCH_TASKS.TEXT_EMBEDDING)
: {})
)
.map((model: TrainedModelConfigResponse) => model.model_id);
return availableTrainedModelsList;
}, [availableTrainedModels]);
return (
<EuiFlexGroup>
<EuiFlexItem>
{inferenceEndpoints.length === 0 ? (
<EmptyPromptPage
addEndpointLabel={addEndpointLabel}
breadcrumbs={inferenceEndpointsBreadcrumbs}
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
/>
) : (
<TabularPage
addEndpointLabel={addEndpointLabel}
breadcrumbs={inferenceEndpointsBreadcrumbs}
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
{isInferenceFlyoutVisible && (
<InferenceFlyoutWrapper
errorCallout={
inferenceAddError && (
<EuiFlexItem grow={false}>
<EuiCallOut
color="danger"
data-test-subj="addInferenceError"
iconType="error"
title={i18n.translate(
'xpack.enterpriseSearch.content.inferenceEndpoints.inferenceId.errorTitle',
{
defaultMessage: 'Error adding inference endpoint',
}
)}
>
<EuiText>
<FormattedMessage
id="xpack.enterpriseSearch.content.inferenceEndpoints.inferenceId.errorDescription"
defaultMessage="Error adding inference endpoint: {errorMessage}"
values={{ errorMessage: inferenceAddError }}
/>
</EuiText>
</EuiCallOut>
<EuiSpacer />
</EuiFlexItem>
)
}
onInferenceEndpointChange={onInferenceEndpointChange}
inferenceEndpointError={inferenceEndpointError}
trainedModels={trainedModels}
onSaveInferenceEndpoint={onSaveInferenceCallback}
onFlyoutClose={onFlyoutClose}
isInferenceFlyoutVisible={isInferenceFlyoutVisible}
supportedNlpModels={docLinks.supportedNlpModels}
nlpImportModel={docLinks.nlpImportModel}
isCreateInferenceApiLoading={isCreateInferenceApiLoading}
setInferenceEndpointError={setInferenceEndpointError}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { Route, Routes } from '@kbn/shared-ux-router';
import { INFERENCE_ENDPOINTS_PATH } from '../../routes';
import { InferenceEndpoints } from './inference_endpoints';
export const InferenceEndpointsRouter: React.FC = () => {
return (
<Routes>
<Route exact path={INFERENCE_ENDPOINTS_PATH}>
<InferenceEndpoints />
</Route>
</Routes>
);
};

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EnterpriseSearchContentPageTemplate } from '../layout/page_template';
interface TabularPageProps {
addEndpointLabel: string;
breadcrumbs: string[];
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const TabularPage: React.FC<TabularPageProps> = ({
addEndpointLabel,
breadcrumbs,
setIsInferenceFlyoutVisible,
}) => (
<EnterpriseSearchContentPageTemplate
pageChrome={breadcrumbs}
pageViewTelemetry="Inference Endpoints"
isLoading={false}
pageHeader={{
description: i18n.translate('xpack.enterpriseSearch.content.inferenceEndpoints.description', {
defaultMessage:
'Manage your Elastic and third-party endpoints generated from the Inference API.',
}),
pageTitle: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.title', {
defaultMessage: 'Inference endpoints',
}),
rightSideGroupProps: {
gutterSize: 's',
responsive: false,
},
rightSideItems: [
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<EuiButton
data-test-subj="entSearchContent-inferenceEndpoints-addButton"
key="newInferenceEndpoint"
color="primary"
iconType="plusInCircle"
fill
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{addEndpointLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>,
],
}}
/>
);

View file

@ -22,11 +22,13 @@ import { VersionMismatchPage } from '../shared/version_mismatch';
import { ConnectorsRouter } from './components/connectors/connectors_router';
import { CrawlersRouter } from './components/connectors/crawlers_router';
import { InferenceEndpointsRouter } from './components/inference_endpoints/inference_endpoints_router';
import { NotFound } from './components/not_found';
import { SearchIndicesRouter } from './components/search_indices';
import {
CONNECTORS_PATH,
CRAWLERS_PATH,
INFERENCE_ENDPOINTS_PATH,
ERROR_STATE_PATH,
ROOT_PATH,
SEARCH_INDICES_PATH,
@ -82,6 +84,9 @@ export const EnterpriseSearchContentConfigured: React.FC<Required<InitialAppData
<Route path={CRAWLERS_PATH}>
<CrawlersRouter />
</Route>
<Route path={INFERENCE_ENDPOINTS_PATH}>
<InferenceEndpointsRouter />
</Route>
<Route>
<NotFound />
</Route>

View file

@ -13,6 +13,7 @@ export const ERROR_STATE_PATH = '/error_state';
export const SEARCH_INDICES_PATH = `${ROOT_PATH}search_indices`;
export const CONNECTORS_PATH = `${ROOT_PATH}connectors`;
export const CRAWLERS_PATH = `${ROOT_PATH}crawlers`;
export const INFERENCE_ENDPOINTS_PATH = `${ROOT_PATH}inference_endpoints`;
export const SETTINGS_PATH = `${ROOT_PATH}settings`;
export const NEW_INDEX_PATH = `${SEARCH_INDICES_PATH}/new_index`;

View file

@ -0,0 +1,108 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiButton,
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import inferenceEndpoint from '../../../assets/images/inference_endpoint.svg';
import { ElserPrompt } from './elser_prompt';
import { MultilingualE5Prompt } from './multilingual_e5_prompt';
interface AddEmptyPromptProps {
addEndpointLabel: string;
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const AddEmptyPrompt: React.FC<AddEmptyPromptProps> = ({
addEndpointLabel,
setIsInferenceFlyoutVisible,
}) => {
return (
<EuiEmptyPrompt
layout="horizontal"
title={
<h2>
<FormattedMessage
id="xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.h2.createFirstInferenceEndpointLabel"
defaultMessage="Inference Endpoints"
/>
</h2>
}
body={
<EuiFlexGroup direction="column">
<EuiFlexItem>
{i18n.translate(
'xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.createFirstInferenceEndpointDescription',
{
defaultMessage:
'Connect to your third-party model provider of choice to setup a single entity for semantic search.',
}
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<div>
<EuiButton
data-test-subj="enterpriseSearchAddEmptyPromptAddEndpointButton"
color="primary"
fill
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{addEndpointLabel}
</EuiButton>
</div>
</EuiFlexItem>
</EuiFlexGroup>
}
footer={
<EuiFlexGroup gutterSize="xs" direction="column">
<EuiFlexItem>
<strong>
{i18n.translate(
'xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.startWithPreparedEndpointsLabel',
{
defaultMessage: 'Quickly start with our prepared endpoints:',
}
)}
</strong>
</EuiFlexItem>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<ElserPrompt
addEndpointLabel={addEndpointLabel}
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
/>
</EuiFlexItem>
<EuiFlexItem>
<MultilingualE5Prompt
addEndpointLabel={addEndpointLabel}
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
}
color="plain"
hasBorder
icon={<EuiImage size="fullWidth" src={inferenceEndpoint} alt="" />}
/>
);
};

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface ElserPromptProps {
addEndpointLabel: string;
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const ElserPrompt: React.FC<ElserPromptProps> = ({
addEndpointLabel,
setIsInferenceFlyoutVisible,
}) => (
<EuiCard
display="primary"
textAlign="left"
description={i18n.translate(
'xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.elserDescription',
{
defaultMessage:
"ELSER is Elastic's NLP model for English semantic search, utilizing sparse vectors.",
}
)}
title={i18n.translate(
'xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.elserTitle',
{
defaultMessage: 'ELSER',
}
)}
footer={
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="enterpriseSearchAddEmptyPromptAddElserButton"
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{addEndpointLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
);

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { AddEmptyPrompt } from './add_empty_prompt';

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
interface MultilingualE5PromptProps {
addEndpointLabel: string;
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const MultilingualE5Prompt: React.FC<MultilingualE5PromptProps> = ({
addEndpointLabel,
setIsInferenceFlyoutVisible,
}) => (
<EuiCard
display="primary"
textAlign="left"
description={i18n.translate(
'xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.e5Description',
{
defaultMessage:
'E5 is a third party NLP model that enables you to perform multi-lingual semantic search by using dense vector representations.',
}
)}
title={i18n.translate(
'xpack.enterpriseSearch.addEmptyPromptForInferenceEndpoint.addEmptyPrompt.e5Title',
{
defaultMessage: 'Multilingual E5',
}
)}
footer={
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="enterpriseSearchAddEmptyPromptAddE5Button"
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{addEndpointLabel}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
}
/>
);

View file

@ -124,6 +124,7 @@ class DocLinks {
public licenseManagement: string;
public machineLearningStart: string;
public mlDocumentEnrichment: string;
public nlpImportModel: string;
public pluginsIngestAttachment: string;
public queryDsl: string;
public restApis: string;
@ -301,6 +302,7 @@ class DocLinks {
this.licenseManagement = '';
this.machineLearningStart = '';
this.mlDocumentEnrichment = '';
this.nlpImportModel = '';
this.pluginsIngestAttachment = '';
this.queryDsl = '';
this.restApis = '';
@ -480,6 +482,7 @@ class DocLinks {
this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement;
this.machineLearningStart = docLinks.links.enterpriseSearch.machineLearningStart;
this.mlDocumentEnrichment = docLinks.links.enterpriseSearch.mlDocumentEnrichment;
this.nlpImportModel = docLinks.links.ml.nlpImportModel;
this.pluginsIngestAttachment = docLinks.links.plugins.ingestAttachment;
this.queryDsl = docLinks.links.query.queryDsl;
this.restApis = docLinks.links.apis.restApis;

View file

@ -32,6 +32,7 @@ import { useIndicesNav } from '../../enterprise_search_content/components/search
import {
CONNECTORS_PATH,
CRAWLERS_PATH,
INFERENCE_ENDPOINTS_PATH,
SEARCH_INDICES_PATH,
} from '../../enterprise_search_content/routes';
import { KibanaLogic } from '../kibana';
@ -96,6 +97,17 @@ export const useEnterpriseSearchNav = () => {
to: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + CRAWLERS_PATH,
}),
},
{
id: 'inference_endpoints',
name: i18n.translate('xpack.enterpriseSearch.nav.inferenceEndpointsTitle', {
defaultMessage: 'Inference Endpoints',
}),
...generateNavLink({
shouldNotCreateHref: true,
shouldShowActiveForSubroutes: true,
to: ENTERPRISE_SEARCH_CONTENT_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH,
}),
},
],
name: i18n.translate('xpack.enterpriseSearch.nav.contentTitle', {
defaultMessage: 'Content',

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 697 KiB

View file

@ -73,6 +73,7 @@
"@kbn/utility-types",
"@kbn/index-management",
"@kbn/deeplinks-search",
"@kbn/inference_integration_flyout",
"@kbn/react-kibana-context-theme",
"@kbn/search-types",
"@kbn/cloud",