Skip to content

[Security Assistant] EIS usage callout #221566

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

Merged
merged 67 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
b080d0e
add EIS cost callout
angorayc May 26, 2025
28f48ce
show EIS callout when it's selected
angorayc May 27, 2025
3506eb5
conversation EIS message
angorayc May 28, 2025
f1cc5b5
use Elastic Managed LLM connector
angorayc May 28, 2025
870f2ce
fix message
angorayc May 28, 2025
3bc8d0a
wording
angorayc May 29, 2025
8ca4174
Merge branch 'main' into issues/12656
angorayc May 29, 2025
f1686d7
add usage cost url
angorayc May 29, 2025
bf24095
Merge branch 'issues/12656' of github.com:angorayc/kibana into issues…
angorayc May 29, 2025
d07f6d5
i18n fix
angorayc May 30, 2025
3857c09
unit tests
angorayc May 30, 2025
9595228
code review
angorayc Jun 2, 2025
dd1a58d
Merge branch 'main' into issues/12656
angorayc Jun 2, 2025
ba4e25d
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 2, 2025
6470381
rm inferenceEnabled
angorayc Jun 3, 2025
1673fb8
types
angorayc Jun 3, 2025
47609c2
unit tests
angorayc Jun 3, 2025
0e86066
fix up
angorayc Jun 3, 2025
c07d353
unit tests
angorayc Jun 3, 2025
ed2a90c
Merge branch 'issues/12656' of github.com:angorayc/kibana into issues…
angorayc Jun 3, 2025
ae4945b
unit tests
angorayc Jun 3, 2025
73917f7
unit test
angorayc Jun 4, 2025
5ceae9e
Merge branch 'main' of github.com:elastic/kibana into issues/12656
angorayc Jun 4, 2025
7c33d1a
use different storage keys for each tour
angorayc Jun 4, 2025
74ad42d
unit tests
angorayc Jun 4, 2025
dda3a25
Merge branch 'main' of github.com:elastic/kibana into issues/12656
angorayc Jun 4, 2025
e5c1d60
lint
angorayc Jun 4, 2025
d2de469
clean up
angorayc Jun 4, 2025
4a872ae
[CI] Auto-commit changed files from 'node scripts/yarn_deduplicate'
kibanamachine Jun 4, 2025
80e14eb
Merge branch 'main' into issues/12656
angorayc Jun 5, 2025
3ff98f5
Merge branch 'main' into issues/12656
angorayc Jun 5, 2025
d4bf753
type
angorayc Jun 5, 2025
10c7314
Merge branch 'issues/12656' of github.com:angorayc/kibana into issues…
angorayc Jun 5, 2025
c2061a4
type
angorayc Jun 5, 2025
625b450
Merge branch 'main' into issues/12656
angorayc Jun 5, 2025
6eaaa77
use docLinksServiceMock
angorayc Jun 5, 2025
51fb3d1
Merge branch 'issues/12656' of github.com:angorayc/kibana into issues…
angorayc Jun 5, 2025
f14f8d8
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 5, 2025
200ef61
Merge branch 'main' of github.com:elastic/kibana into issues/12656
angorayc Jun 9, 2025
9650631
Update x-pack/platform/packages/shared/kbn-elastic-assistant/impl/ass…
angorayc Jun 9, 2025
eb4c539
Update x-pack/solutions/security/plugins/security_solution/public/onb…
angorayc Jun 9, 2025
92e2bca
review
angorayc Jun 9, 2025
ec64ef5
Merge branch 'issues/12656' of github.com:angorayc/kibana into issues…
angorayc Jun 9, 2025
a6239c4
Update x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tou…
angorayc Jun 9, 2025
5d6bea5
fix up
angorayc Jun 9, 2025
aea08c8
fix up
angorayc Jun 9, 2025
5d24e64
lint
angorayc Jun 9, 2025
3e06ade
rm esUiShared
angorayc Jun 9, 2025
1a0893d
unit tests
angorayc Jun 9, 2025
7e6d6c4
Merge branch 'main' of github.com:elastic/kibana into issues/12656
angorayc Jun 10, 2025
cd21fea
connector flyout
angorayc Jun 10, 2025
9fa6031
i18n
angorayc Jun 10, 2025
3a894d4
update i18n
angorayc Jun 11, 2025
7af2ce0
copy
angorayc Jun 11, 2025
3edb5fb
lint
angorayc Jun 11, 2025
cde945b
Update x-pack/platform/packages/shared/kbn-elastic-assistant/impl/ass…
angorayc Jun 12, 2025
e521150
Update x-pack/platform/packages/shared/kbn-elastic-assistant/impl/tou…
angorayc Jun 12, 2025
554959d
Update x-pack/platform/packages/shared/kbn-elastic-assistant/impl/ass…
angorayc Jun 12, 2025
a4a877d
rm unused i18n
angorayc Jun 12, 2025
59e895f
Merge branch 'main' into issues/12656
angorayc Jun 12, 2025
8a0bf6e
Merge branch 'main' of github.com:elastic/kibana into issues/12656
angorayc Jun 12, 2025
52aabb7
hide AnonymizedValuesAndCitationsTour when eis tour is active
angorayc Jun 13, 2025
6dd7aa3
update links
angorayc Jun 13, 2025
0e47f80
tests
angorayc Jun 13, 2025
bcebe8c
Merge branch 'main' into issues/12656
angorayc Jun 15, 2025
3a5eb5b
Merge branch 'main' into issues/12656
angorayc Jun 16, 2025
0c221b1
Merge branch 'main' into issues/12656
angorayc Jun 16, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
avcResults: `https://www.elastic.co/blog/elastic-security-av-comparatives-business-test`,
bidirectionalIntegrations: `${ELASTIC_DOCS}solutions/security/endpoint-response-actions/third-party-response-actions`,
trustedApps: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/trusted-applications`,
elasticAiFeatures: `${ELASTIC_DOCS}solutions/security/ai`,
eventFilters: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/event-filters`,
blocklist: `${ELASTIC_DOCS}solutions/security/manage-elastic-defend/blocklist`,
threatIntelInt: `${ELASTIC_DOCS}solutions/security/get-started/enable-threat-intelligence-integrations`,
Expand Down Expand Up @@ -462,6 +463,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
: `https://www.elastic.co/blog/security-prebuilt-rules-editing`,
createEsqlRuleType: `${ELASTIC_DOCS}solutions/security/detect-and-alert/create-detection-rule#create-esql-rule`,
ruleUiAdvancedParams: `${ELASTIC_DOCS}solutions/security/detect-and-alert/create-detection-rule#rule-ui-advanced-params`,
thirdPartyLlmProviders: `${ELASTIC_DOCS}solutions/security/ai/set-up-connectors-for-large-language-models-llm`,
entityAnalytics: {
riskScorePrerequisites: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring-requirements`,
entityRiskScoring: `${ELASTIC_DOCS}solutions/security/advanced-entity-analytics/entity-risk-scoring`,
Expand Down Expand Up @@ -600,6 +602,8 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
apmRulesTransactionError: `${ELASTIC_DOCS}solutions/observability/incident-management/create-failed-transaction-rate-threshold-rule`,
apmRulesAnomaly: `${ELASTIC_DOCS}solutions/observability/incident-management/create-an-apm-anomaly-rule`,
authorization: `${KIBANA_DOCS}alerting-setup.html#alerting-authorization`,
elasticManagedLlm: `${ELASTIC_DOCS}reference/kibana/connectors-kibana/elastic-managed-llm`,
elasticManagedLlmUsageCost: `${ELASTIC_WEBSITE_URL}pricing`,
emailAction: `${ELASTIC_DOCS}reference/kibana/connectors-kibana/email-action-type`,
emailActionConfig: `${ELASTIC_DOCS}reference/kibana/connectors-kibana/email-action-type`,
emailExchangeClientSecretConfig: `${ELASTIC_DOCS}reference/kibana/connectors-kibana/email-action-type#exchange-client-secret`,
Expand Down
4 changes: 4 additions & 0 deletions src/platform/packages/shared/kbn-doc-links/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,9 @@ export interface DocLinks {
readonly artifactControl: string;
readonly avcResults: string;
readonly bidirectionalIntegrations: string;
readonly thirdPartyLlmProviders: string;
readonly trustedApps: string;
readonly elasticAiFeatures: string;
readonly eventFilters: string;
readonly eventMerging: string;
readonly blocklist: string;
Expand Down Expand Up @@ -434,6 +436,8 @@ export interface DocLinks {
slackApiAction: string;
teamsAction: string;
connectors: string;
elasticManagedLlm: string;
elasticManagedLlmUsageCost: string;
}>;
readonly taskManager: Readonly<{
healthMonitoring: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { render } from '@testing-library/react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { ElasticLlmCallout } from './elastic_llm_callout';
import { TestProviders } from '../../mock/test_providers/test_providers';

jest.mock('react-use/lib/useLocalStorage');

describe('ElasticLlmCallout', () => {
const defaultProps = {
showEISCallout: true,
};

beforeEach(() => {
jest.clearAllMocks();
(useLocalStorage as jest.Mock).mockReturnValue([false, jest.fn()]);
});

it('should not render when showEISCallout is false', () => {
const { queryByTestId } = render(<ElasticLlmCallout showEISCallout={false} />, {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument();
});

it('should not render when tour is completed', () => {
(useLocalStorage as jest.Mock).mockReturnValue([true, jest.fn()]);
const { queryByTestId } = render(
<TestProviders>
<ElasticLlmCallout {...defaultProps} />
</TestProviders>,
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);

expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument();
});

it('should render links', () => {
const { queryByTestId } = render(
<TestProviders>
<ElasticLlmCallout {...defaultProps} />
</TestProviders>,
{ wrapper: ({ children }) => <TestProviders>{children}</TestProviders> }
);
expect(queryByTestId('elasticLlmUsageCostLink')).toHaveTextContent('additional costs incur');
expect(queryByTestId('elasticLlmConnectorLink')).toHaveTextContent('connector');
expect(queryByTestId('elasticLlmSettingsLink')).toHaveTextContent('Settings');
});

it('should show callout when showEISCallout changes to true', () => {
const { rerender, queryByTestId } = render(
<TestProviders>
<ElasticLlmCallout showEISCallout={false} />
</TestProviders>,
{ wrapper: ({ children }) => <TestProviders>{children}</TestProviders> }
);
expect(queryByTestId('elasticLlmCallout')).not.toBeInTheDocument();

rerender(<ElasticLlmCallout showEISCallout={true} />);
expect(queryByTestId('elasticLlmCallout')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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, { useCallback, useEffect, useState } from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';

import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiLink, useEuiTheme } from '@elastic/eui';
import { useAssistantContext } from '../../assistant_context';
import { useAssistantSpaceId } from '../use_space_aware_context';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../tour/const';
import { useTourStorageKey } from '../../tour/common/hooks/use_tour_storage_key';

export const ElasticLlmCallout = ({ showEISCallout }: { showEISCallout: boolean }) => {
const {
getUrlForApp,
docLinks: {
links: {
alerting: { elasticManagedLlmUsageCost: ELASTIC_LLM_USAGE_COST_LINK },
},
},
} = useAssistantContext();
const spaceId = useAssistantSpaceId();
const { euiTheme } = useEuiTheme();
const tourStorageKey = useTourStorageKey(
NEW_FEATURES_TOUR_STORAGE_KEYS.CONVERSATION_CONNECTOR_ELASTIC_LLM
);
const [tourCompleted, setTourCompleted] = useLocalStorage<boolean>(tourStorageKey, false);
const [showCallOut, setShowCallOut] = useState<boolean>(showEISCallout);

const onDismiss = useCallback(() => {
setShowCallOut(false);
setTourCompleted(true);
}, [setTourCompleted]);

useEffect(() => {
if (showEISCallout && !tourCompleted) {
setShowCallOut(true);
} else {
setShowCallOut(false);
}
}, [showEISCallout, tourCompleted]);

if (!showCallOut) {
return;
}

return (
<EuiCallOut
data-test-subj="elasticLlmCallout"
onDismiss={onDismiss}
iconType="iInCircle"
title={i18n.translate('xpack.elasticAssistant.assistant.connectors.elasticLlmCallout.title', {
defaultMessage: 'You are now using the Elastic-managed LLM connector',
})}
size="s"
css={css`
padding: ${euiTheme.size.s} !important;
`}
>
<p>
<FormattedMessage
id="xpack.elasticAssistant.assistant.connectors.tour.elasticLlmDescription"
defaultMessage="A large language model (LLM) is required to power the AI Assistant and AI-driven features in Elastic. By default, Elastic uses its Elastic-managed LLM connector ({costLink}) when no custom connectors are available. You can always configure and use your own {connectorLink} and change the default in {settingsLink}."
values={{
costLink: (
<EuiLink
data-test-subj="elasticLlmUsageCostLink"
href={ELASTIC_LLM_USAGE_COST_LINK}
target="_blank"
rel="noopener noreferrer"
external
>
<FormattedMessage
id="xpack.elasticAssistant.assistant.eisCallout.extraCost.label"
defaultMessage="additional costs incur"
/>
</EuiLink>
),
connectorLink: (
<EuiLink
data-test-subj="elasticLlmConnectorLink"
href={getUrlForApp('management', {
path: `/insightsAndAlerting/triggersActionsConnectors/connectors`,
})}
target="_blank"
rel="noopener noreferrer"
external
>
<FormattedMessage
id="xpack.elasticAssistant.assistant.eisCallout.connector.label"
defaultMessage="connector"
/>
</EuiLink>
),
settingsLink: (
<EuiLink
data-test-subj="elasticLlmSettingsLink"
href={getUrlForApp('management', {
path: `/kibana/spaces/edit/${spaceId}`,
})}
target="_blank"
rel="noopener noreferrer"
external
>
<FormattedMessage
id="xpack.elasticAssistant.assistant.eisCallout.settings.label"
defaultMessage="Settings"
/>
</EuiLink>
),
}}
/>
</p>
</EuiCallOut>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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 { render, screen } from '@testing-library/react';
import { AssistantConversationBanner } from '.';
import { AIConnector, Conversation, useAssistantContext } from '../../..';
import { customConvo } from '../../mock/conversation';

jest.mock('../../..');

jest.mock('../../connectorland/connector_missing_callout', () => ({
ConnectorMissingCallout: () => <div data-test-subj="connector-missing-callout" />,
}));

jest.mock('./elastic_llm_callout', () => ({
ElasticLlmCallout: () => <div data-test-subj="elastic-llm-callout" />,
}));

describe('AssistantConversationBanner', () => {
const setIsSettingsModalVisible = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
});

it('renders ConnectorMissingCallout when shouldShowMissingConnectorCallout is true', () => {
(useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: true });

render(
<AssistantConversationBanner
isSettingsModalVisible={false}
setIsSettingsModalVisible={setIsSettingsModalVisible}
shouldShowMissingConnectorCallout={true}
currentConversation={undefined}
connectors={[]}
/>
);

expect(screen.getByTestId('connector-missing-callout')).toBeInTheDocument();
});

it('renders ElasticLlmCallout when Elastic LLM is enabled', () => {
(useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: true });
const mockConnectors = [
{ id: 'mockLLM', actionTypeId: '.inference', isPreconfigured: true },
] as AIConnector[];

const mockConversation = {
...customConvo,
id: 'mockConversation',
apiConfig: {
connectorId: 'mockLLM',
actionTypeId: '.inference',
},
} as Conversation;

render(
<AssistantConversationBanner
isSettingsModalVisible={false}
setIsSettingsModalVisible={setIsSettingsModalVisible}
shouldShowMissingConnectorCallout={false}
currentConversation={mockConversation}
connectors={mockConnectors}
/>
);

expect(screen.getByTestId('elastic-llm-callout')).toBeInTheDocument();
});

it('renders nothing when no conditions are met', () => {
(useAssistantContext as jest.Mock).mockReturnValue({ inferenceEnabled: false });

const { container } = render(
<AssistantConversationBanner
isSettingsModalVisible={false}
setIsSettingsModalVisible={setIsSettingsModalVisible}
shouldShowMissingConnectorCallout={false}
currentConversation={undefined}
connectors={[]}
/>
);

expect(container).toBeEmptyDOMElement();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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, { useMemo } from 'react';
import { AIConnector, Conversation, useAssistantContext } from '../../..';
import { isElasticManagedLlmConnector } from '../../connectorland/helpers';
import { ConnectorMissingCallout } from '../../connectorland/connector_missing_callout';
import { ElasticLlmCallout } from './elastic_llm_callout';

export const AssistantConversationBanner = React.memo(
({
isSettingsModalVisible,
setIsSettingsModalVisible,
shouldShowMissingConnectorCallout,
currentConversation,
connectors,
}: {
isSettingsModalVisible: boolean;
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
shouldShowMissingConnectorCallout: boolean;
currentConversation: Conversation | undefined;
connectors: AIConnector[] | undefined;
}) => {
const { inferenceEnabled } = useAssistantContext();
const showEISCallout = useMemo(() => {
if (inferenceEnabled && currentConversation && currentConversation.id !== '') {
if (currentConversation?.apiConfig?.connectorId) {
return connectors?.some(
(c) =>
c.id === currentConversation.apiConfig?.connectorId && isElasticManagedLlmConnector(c)
);
}
}
}, [inferenceEnabled, currentConversation, connectors]);
if (shouldShowMissingConnectorCallout) {
return (
<ConnectorMissingCallout
isConnectorConfigured={(connectors?.length ?? 0) > 0}
isSettingsModalVisible={isSettingsModalVisible}
setIsSettingsModalVisible={setIsSettingsModalVisible}
/>
);
}

if (showEISCallout) {
return <ElasticLlmCallout showEISCallout={showEISCallout} />;
}

return null;
}
);

AssistantConversationBanner.displayName = 'AssistantConversationBanner';
Loading