Skip to content

Commit

Permalink
[ML] Data Frame Analytics: add test pipeline action for DFA trained m…
Browse files Browse the repository at this point in the history
…odels in models list (#168400)

## Summary

Related issue: [Testing data frame analysis models
UI](#164440)

This PR:

- Adds ability to test the DFA model directly by adding the 'Test model'
action
- Added a link to the simulate pipeline API via 'Learn more'
- 'View request body' allows the user to view the request being sent to
the simulate pipeline endpoint
- Checks if the source index exists and adds a dismissible callout with
warning text
- Adds a 'reload' option that gets a random sample doc to test
- The 'reset' button resets the sample doc to the last used sample doc

<img width="1614" alt="image"
src="https://github.com/elastic/kibana/assets/6446462/f37430db-5ef4-4088-859c-9f75dd2a14ab">

<img width="1697" alt="image"
src="https://github.com/elastic/kibana/assets/6446462/ad6b785f-6e81-437c-93a6-694d19f43e67">

<img width="1624" alt="image"
src="https://github.com/elastic/kibana/assets/6446462/cbac54b2-b9ed-41f0-8a80-bf7ed40f723f">



### Checklist

Delete any items that are not applicable to this PR.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
alvarezmelissa87 and kibanamachine committed Oct 17, 2023
1 parent 382e3f6 commit a304007
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 77 deletions.
Expand Up @@ -33,7 +33,7 @@ import { ReviewAndCreatePipeline } from './components/review_and_create_pipeline
import { useMlApiContext } from '../../contexts/kibana';
import { getPipelineConfig } from './get_pipeline_config';
import { validateInferencePipelineConfigurationStep } from './validation';
import type { MlInferenceState, InferenceModelTypes } from './types';
import { type MlInferenceState, type InferenceModelTypes, TEST_PIPELINE_MODE } from './types';
import { useFetchPipelines } from './hooks/use_fetch_pipelines';

export interface AddInferencePipelineFlyoutProps {
Expand Down Expand Up @@ -157,7 +157,11 @@ export const AddInferencePipelineFlyout: FC<AddInferencePipelineFlyoutProps> = (
/>
)}
{step === ADD_INFERENCE_PIPELINE_STEPS.TEST && (
<TestPipeline sourceIndex={sourceIndex} state={formState} />
<TestPipeline
sourceIndex={sourceIndex}
state={formState}
mode={TEST_PIPELINE_MODE.STEP}
/>
)}
{step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && (
<ReviewAndCreatePipeline
Expand Down
Expand Up @@ -5,24 +5,28 @@
* 2.0.
*/

import React, { FC, memo, useEffect, useCallback, useState } from 'react';
import React, { FC, memo, useEffect, useCallback, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';

import {
EuiAccordion,
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiCode,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiResizableContainer,
EuiSpacer,
EuiTitle,
EuiText,
useIsWithinMaxBreakpoint,
EuiPanel,
htmlIdGenerator,
} from '@elastic/eui';

import { IngestSimulateDocument } from '@elastic/elasticsearch/lib/api/types';
Expand All @@ -35,32 +39,57 @@ import { useMlApiContext, useMlKibana } from '../../../contexts/kibana';
import { getPipelineConfig } from '../get_pipeline_config';
import { isValidJson } from '../../../../../common/util/validation_utils';
import type { MlInferenceState } from '../types';
import { checkIndexExists } from '../retry_create_data_view';
import { type TestPipelineMode, TEST_PIPELINE_MODE } from '../types';

const sourceIndexMissingMessage = i18n.translate(
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.sourceIndexMissing',
{
defaultMessage:
'The source index used to train the model is missing. Enter text for documents to test the pipeline.',
}
);

interface Props {
sourceIndex?: string;
state: MlInferenceState;
mode: TestPipelineMode;
}

export const TestPipeline: FC<Props> = memo(({ state, sourceIndex }) => {
export const TestPipeline: FC<Props> = memo(({ state, sourceIndex, mode }) => {
const [simulatePipelineResult, setSimulatePipelineResult] = useState<
undefined | estypes.IngestSimulateResponse
>();
const [simulatePipelineError, setSimulatePipelineError] = useState<undefined | string>();
const [sourceIndexMissingError, setSourceIndexMissingError] = useState<undefined | string>();
const [sampleDocsString, setSampleDocsString] = useState<string>('');
const [lastFetchedSampleDocsString, setLastFetchedSampleDocsString] = useState<string>('');
const [isValid, setIsValid] = useState<boolean>(true);
const [showCallOut, setShowCallOut] = useState<boolean>(true);
const {
esSearch,
trainedModels: { trainedModelPipelineSimulate },
} = useMlApiContext();
const {
notifications: { toasts },
services: {
docLinks: { links },
},
} = useMlKibana();

const isSmallerViewport = useIsWithinMaxBreakpoint('s');
const accordionId = useMemo(() => htmlIdGenerator()(), []);
const pipelineConfig = useMemo(() => getPipelineConfig(state), [state]);
const requestBody = useMemo(() => {
const body = { pipeline: pipelineConfig, docs: [] };
if (isValidJson(sampleDocsString)) {
body.docs = JSON.parse(sampleDocsString);
}
return body;
}, [pipelineConfig, sampleDocsString]);

const simulatePipeline = async () => {
try {
const pipelineConfig = getPipelineConfig(state);
const result = await trainedModelPipelineSimulate(
pipelineConfig,
JSON.parse(sampleDocsString) as IngestSimulateDocument[]
Expand Down Expand Up @@ -95,36 +124,79 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex }) => {
setIsValid(valid);
};

const getSampleDocs = useCallback(async () => {
let records: IngestSimulateDocument[] = [];
let resp;
const getDocs = useCallback(
async (body: any) => {
let records: IngestSimulateDocument[] = [];
let resp;
try {
resp = await esSearch(body);

try {
resp = await esSearch({
index: sourceIndex,
body: {
size: 1,
},
});
if (resp && resp.hits.total.value > 0) {
records = resp.hits.hits;
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
setSampleDocsString(JSON.stringify(records, null, 2));
setSimulatePipelineResult(undefined);
setLastFetchedSampleDocsString(JSON.stringify(records, null, 2));
setIsValid(true);
},
[esSearch]
);

const { getSampleDoc, getRandomSampleDoc } = useMemo(
() => ({
getSampleDoc: async () => {
getDocs({
index: sourceIndex,
body: {
size: 1,
},
});
},
getRandomSampleDoc: async () => {
getDocs({
index: sourceIndex,
body: {
size: 1,
query: {
function_score: {
query: { match_all: {} },
random_score: {},
},
},
},
});
},
}),
[getDocs, sourceIndex]
);

if (resp && resp.hits.total.value > 0) {
records = resp.hits.hits;
useEffect(
function checkSourceIndexExists() {
async function ensureSourceIndexExists() {
const resp = await checkIndexExists(sourceIndex!);
const indexExists = resp.resp && resp.resp[sourceIndex!] && resp.resp[sourceIndex!].exists;
if (indexExists === false) {
setSourceIndexMissingError(sourceIndexMissingMessage);
}
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
setSampleDocsString(JSON.stringify(records, null, 2));
setIsValid(true);
}, [sourceIndex, esSearch]);
if (sourceIndex) {
ensureSourceIndexExists();
}
},
[sourceIndex, sourceIndexMissingError]
);

useEffect(
function fetchSampleDocsFromSource() {
if (sourceIndex) {
getSampleDocs();
if (sourceIndex && sourceIndexMissingError === undefined) {
getSampleDoc();
}
},
[sourceIndex, getSampleDocs]
[sourceIndex, getSampleDoc, sourceIndexMissingError]
);

return (
Expand All @@ -147,27 +219,88 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex }) => {
<EuiFlexItem>
<EuiText color="subdued" size="s">
<p>
<strong>
{i18n.translate(
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.optionalCallout',
{ defaultMessage: 'This is an optional step.' }
)}
</strong>
&nbsp;
<FormattedMessage
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.description"
defaultMessage="Run a simulation of the pipeline to confirm it produces the anticipated results."
/>{' '}
{state.targetField && (
{mode === TEST_PIPELINE_MODE.STEP ? (
<>
<strong>
{i18n.translate(
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.optionalCallout',
{ defaultMessage: 'This is an optional step.' }
)}
</strong>
&nbsp;
</>
) : null}
<>
<FormattedMessage
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.targetFieldHint"
defaultMessage="Check for the target field {targetField} for the prediction in the Result tab."
values={{ targetField: <EuiCode>{state.targetField}</EuiCode> }}
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.description"
defaultMessage="Run a simulation of the pipeline to confirm it produces the anticipated results. {simulatePipelineDocsLink}"
values={{
simulatePipelineDocsLink: (
<EuiLink external target="_blank" href={links.apis.simulatePipeline}>
Learn more.
</EuiLink>
),
}}
/>
<br />
</>
{state.targetField && (
<>
<FormattedMessage
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.targetFieldHint"
defaultMessage="Check for the target field {targetField} for the prediction in the Result tab."
values={{
targetField: <EuiCode>{state.targetField}</EuiCode>,
}}
/>
<br />
</>
)}
{sampleDocsString && sourceIndexMissingError === undefined ? (
<FormattedMessage
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.sourceIndexDocText"
defaultMessage="The provided sample document is taken from the source index used to train the model."
/>
) : null}
</p>
</EuiText>
</EuiFlexItem>
<EuiSpacer size="m" />
{sourceIndexMissingError && showCallOut ? (
<EuiFlexItem>
<EuiCallOut
onDismiss={() => {
setShowCallOut(false);
}}
size="s"
title={sourceIndexMissingError}
iconType="warning"
/>
<EuiSpacer size="s" />
</EuiFlexItem>
) : null}
{mode === TEST_PIPELINE_MODE.STAND_ALONE ? (
<EuiFlexItem>
<EuiAccordion
id={accordionId}
buttonContent={
<FormattedMessage
id="xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.viewRequest"
defaultMessage="View request body"
/>
}
>
<EuiCodeBlock
language="json"
isCopyable
overflowHeight="400px"
data-test-subj="mlTrainedModelsInferenceTestStepConfigBlock"
>
{JSON.stringify(requestBody, null, 2)}
</EuiCodeBlock>
</EuiAccordion>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiPanel hasBorder={false} hasShadow={false}>
Expand Down Expand Up @@ -202,12 +335,26 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex }) => {
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
onClick={getSampleDocs}
disabled={sampleDocsString === ''}
onClick={() => setSampleDocsString(lastFetchedSampleDocsString)}
disabled={
sampleDocsString === '' || sampleDocsString === lastFetchedSampleDocsString
}
>
{i18n.translate(
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.resetSampleDocsButton',
{ defaultMessage: 'Reset sample docs' }
{ defaultMessage: 'Reset' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="xs"
onClick={getRandomSampleDoc}
disabled={sampleDocsString === ''}
>
{i18n.translate(
'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.reloadSampleDocsButton',
{ defaultMessage: 'Reload' }
)}
</EuiButtonEmpty>
</EuiFlexItem>
Expand Down Expand Up @@ -261,8 +408,6 @@ export const TestPipeline: FC<Props> = memo(({ state, sourceIndex }) => {
/>
</EuiResizablePanel>

<EuiResizableButton />

<EuiResizablePanel grow={false} hasBorder initialSize={50} paddingSize="xs">
<EuiCodeBlock
language="json"
Expand Down
Expand Up @@ -46,3 +46,9 @@ export interface AdditionalSettings {
condition?: string;
tag?: string;
}

export const TEST_PIPELINE_MODE = {
STAND_ALONE: 'stand_alone',
STEP: 'step',
} as const;
export type TestPipelineMode = typeof TEST_PIPELINE_MODE[keyof typeof TEST_PIPELINE_MODE];

0 comments on commit a304007

Please sign in to comment.