[AI Assistant] Add assistant to Serverless Search (#196832)

## Summary

This adds the AI assistant to Serverless Elasticsearch. It also disables
the knowledge base, and disables a few config values we don't want users
to be able to set in that context.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elena Shostak <165678770+elena-shostak@users.noreply.github.com>
This commit is contained in:
Sander Philipse 2024-10-25 12:03:04 +02:00 committed by GitHub
parent 27a98aa9c3
commit 3bc5e2db73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 642 additions and 352 deletions

View file

@ -10,7 +10,6 @@ xpack.observability.enabled: false
xpack.securitySolution.enabled: false
xpack.serverless.observability.enabled: false
enterpriseSearch.enabled: false
xpack.observabilityAIAssistant.enabled: false
xpack.osquery.enabled: false
# Enable fleet on search projects for agentless features
@ -120,4 +119,16 @@ xpack.searchInferenceEndpoints.ui.enabled: false
xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json
# Semantic text UI
xpack.index_management.dev.enableSemanticText: true
# AI Assistant config
xpack.observabilityAIAssistant.enabled: true
xpack.searchAssistant.enabled: true
xpack.searchAssistant.ui.enabled: true
xpack.observabilityAIAssistant.scope: "search"
xpack.observabilityAIAssistant.enableKnowledgeBase: false
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
xpack.observabilityAiAssistantManagement.logSourcesEnabled: false
xpack.observabilityAiAssistantManagement.spacesEnabled: false
xpack.observabilityAiAssistantManagement.visibilityEnabled: false

View file

@ -183,6 +183,7 @@ xpack.apm.featureFlags.storageExplorerAvailable: false
## Set the AI Assistant type
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
xpack.observabilityAIAssistant.scope: "observability"
# Specify in telemetry the project type
telemetry.labels.serverless: observability

View file

@ -362,6 +362,9 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.observability_onboarding.ui.enabled (boolean?)',
'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)',
'xpack.observabilityAIAssistant.scope (observability?|search?)',
'xpack.observabilityAiAssistantManagement.logSourcesEnabled (boolean?)',
'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)',
'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)',
'share.new_version.enabled (boolean?)',
'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)',
/**

View file

@ -18,6 +18,7 @@ import {
import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public';
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
import { useKibana } from '../hooks/use_kibana';
import { useKnowledgeBase } from '../hooks';
export function ChatActionsMenu({
connectors,
@ -31,6 +32,7 @@ export function ChatActionsMenu({
onCopyConversationClick: () => void;
}) {
const { application, http } = useKibana().services;
const knowledgeBase = useKnowledgeBase();
const [isOpen, setIsOpen] = useState(false);
const handleNavigateToConnectors = () => {
@ -91,15 +93,19 @@ export function ChatActionsMenu({
defaultMessage: 'Actions',
}),
items: [
{
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', {
defaultMessage: 'Manage knowledge base',
}),
onClick: () => {
toggleActionsMenu();
handleNavigateToSettingsKnowledgeBase();
},
},
...(knowledgeBase?.status.value?.enabled
? [
{
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', {
defaultMessage: 'Manage knowledge base',
}),
onClick: () => {
toggleActionsMenu();
handleNavigateToSettingsKnowledgeBase();
},
},
]
: []),
{
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', {
defaultMessage: 'AI Assistant Settings',

View file

@ -37,6 +37,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
loading: false,
value: {
ready: true,
enabled: true,
},
refresh: () => {},
},

View file

@ -123,7 +123,7 @@ export function ChatBody({
showLinkToConversationsApp: boolean;
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void;
navigateToConversation: (conversationId?: string) => void;
navigateToConversation?: (conversationId?: string) => void;
}) {
const license = useLicense();
const hasCorrectLicense = license?.hasAtLeast('enterprise');

View file

@ -53,7 +53,7 @@ export function ChatFlyout({
initialFlyoutPositionMode?: FlyoutPositionMode;
isOpen: boolean;
onClose: () => void;
navigateToConversation(conversationId?: string): void;
navigateToConversation?: (conversationId?: string) => void;
}) {
const { euiTheme } = useEuiTheme();
const breakpoint = useCurrentEuiBreakpoint();
@ -272,10 +272,14 @@ export function ChatFlyout({
conversationList.conversations.refresh();
}}
onToggleFlyoutPositionMode={handleToggleFlyoutPositionMode}
navigateToConversation={(newConversationId?: string) => {
if (onClose) onClose();
navigateToConversation(newConversationId);
}}
navigateToConversation={
navigateToConversation
? (newConversationId?: string) => {
if (onClose) onClose();
navigateToConversation(newConversationId);
}
: undefined
}
/>
</EuiFlexItem>

View file

@ -60,7 +60,7 @@ export function ChatHeader({
onCopyConversation: () => void;
onSaveTitle: (title: string) => void;
onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void;
navigateToConversation: (nextConversationId?: string) => void;
navigateToConversation?: (nextConversationId?: string) => void;
}) {
const theme = useEuiTheme();
const breakpoint = useCurrentEuiBreakpoint();
@ -164,31 +164,32 @@ export function ChatHeader({
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
anchorPosition="downLeft"
button={
<EuiToolTip
content={i18n.translate(
'xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel',
{ defaultMessage: 'Navigate to conversations' }
)}
display="block"
>
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel',
{navigateToConversation ? (
<EuiFlexItem grow={false}>
<EuiPopover
anchorPosition="downLeft"
button={
<EuiToolTip
content={i18n.translate(
'xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel',
{ defaultMessage: 'Navigate to conversations' }
)}
data-test-subj="observabilityAiAssistantChatHeaderButton"
iconType="discuss"
onClick={() => navigateToConversation(conversationId)}
/>
</EuiToolTip>
}
/>
</EuiFlexItem>
display="block"
>
<EuiButtonIcon
aria-label={i18n.translate(
'xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel',
{ defaultMessage: 'Navigate to conversations' }
)}
data-test-subj="observabilityAiAssistantChatHeaderButton"
iconType="discuss"
onClick={() => navigateToConversation(conversationId)}
/>
</EuiToolTip>
}
/>
</EuiFlexItem>
) : null}
</>
) : null}

View file

@ -58,6 +58,7 @@ const defaultProps: ComponentProps<typeof Component> = {
loading: false,
value: {
ready: true,
enabled: true,
},
refresh: () => {},
},

View file

@ -24,6 +24,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
loading: false,
value: {
ready: false,
enabled: true,
},
refresh: () => {},
},
@ -43,12 +44,15 @@ export const Loading: ComponentStoryObj<typeof Component> = merge({}, defaultPro
});
export const NotInstalled: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
args: { knowledgeBase: { status: { loading: false, value: { ready: false } } } },
args: { knowledgeBase: { status: { loading: false, value: { ready: false, enabled: true } } } },
});
export const Installing: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
args: {
knowledgeBase: { status: { loading: false, value: { ready: false } }, isInstalling: true },
knowledgeBase: {
status: { loading: false, value: { ready: false, enabled: true } },
isInstalling: true,
},
},
});
@ -57,7 +61,7 @@ export const InstallError: ComponentStoryObj<typeof Component> = merge({}, defau
knowledgeBase: {
status: {
loading: false,
value: { ready: false },
value: { ready: false, enabled: true },
},
isInstalling: false,
installError: new Error(),
@ -66,5 +70,5 @@ export const InstallError: ComponentStoryObj<typeof Component> = merge({}, defau
});
export const Installed: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
args: { knowledgeBase: { status: { loading: false, value: { ready: true } } } },
args: { knowledgeBase: { status: { loading: false, value: { ready: true, enabled: true } } } },
});

View file

@ -85,8 +85,9 @@ export function WelcomeMessage({
connectors={connectors}
onSetupConnectorClick={handleConnectorClick}
/>
<WelcomeMessageKnowledgeBase connectors={connectors} knowledgeBase={knowledgeBase} />
{knowledgeBase.status.value?.enabled ? (
<WelcomeMessageKnowledgeBase connectors={connectors} knowledgeBase={knowledgeBase} />
) : null}
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -25,7 +25,7 @@ const SECOND_SLOT_CONTAINER_WIDTH = 400;
interface ConversationViewProps {
conversationId?: string;
navigateToConversation: (nextConversationId?: string) => void;
navigateToConversation?: (nextConversationId?: string) => void;
getConversationHref?: (conversationId: string) => string;
newConversationHref?: string;
scopes?: AssistantScope[];
@ -81,7 +81,9 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
const handleConversationUpdate = (conversation: { conversation: { id: string } }) => {
if (!conversationId) {
updateConversationIdInPlace(conversation.conversation.id);
navigateToConversation(conversation.conversation.id);
if (navigateToConversation) {
navigateToConversation(conversation.conversation.id);
}
}
handleRefreshConversations();
};
@ -143,7 +145,7 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
isLoading={conversationList.isLoading}
onConversationDeleteClick={(deletedConversationId) => {
conversationList.deleteConversation(deletedConversationId).then(() => {
if (deletedConversationId === conversationId) {
if (deletedConversationId === conversationId && navigateToConversation) {
navigateToConversation(undefined);
}
});

View file

@ -17,6 +17,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult {
error: undefined,
value: {
ready: true,
enabled: true,
},
},
};

View file

@ -20,6 +20,7 @@ import { useAIAssistantAppService } from './use_ai_assistant_app_service';
export interface UseKnowledgeBaseResult {
status: AbortableAsyncState<{
ready: boolean;
enabled: boolean;
error?: any;
deployment_state?: MlDeploymentState;
allocation_state?: MlDeploymentAllocationState;

View file

@ -11,6 +11,7 @@ export const config = schema.object({
enabled: schema.boolean({ defaultValue: true }),
modelId: schema.maybe(schema.string()),
scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])),
enableKnowledgeBase: schema.boolean({ defaultValue: true }),
});
export type ObservabilityAIAssistantConfig = TypeOf<typeof config>;

View file

@ -159,11 +159,14 @@ export class ObservabilityAIAssistantPlugin
core,
taskManager: plugins.taskManager,
getModelId,
enableKnowledgeBase: this.config.enableKnowledgeBase,
}));
service.register(registerFunctions);
addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') });
if (this.config.enableKnowledgeBase) {
addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') });
}
registerServerRoutes({
core,

View file

@ -28,6 +28,7 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({
handler: async (
resources
): Promise<{
enabled: boolean;
ready: boolean;
error?: any;
deployment_state?: MlDeploymentState;

View file

@ -707,14 +707,16 @@ export class ObservabilityAIAssistantClient {
queries: Array<{ text: string; boost?: number }>;
categories?: string[];
}): Promise<{ entries: RecalledEntry[] }> => {
return this.dependencies.knowledgeBaseService.recall({
namespace: this.dependencies.namespace,
user: this.dependencies.user,
queries,
categories,
esClient: this.dependencies.esClient,
uiSettingsClient: this.dependencies.uiSettingsClient,
});
return (
this.dependencies.knowledgeBaseService?.recall({
namespace: this.dependencies.namespace,
user: this.dependencies.user,
queries,
categories,
esClient: this.dependencies.esClient,
uiSettingsClient: this.dependencies.uiSettingsClient,
}) || { entries: [] }
);
};
getKnowledgeBaseStatus = () => {

View file

@ -70,6 +70,7 @@ export class ObservabilityAIAssistantService {
private readonly logger: Logger;
private readonly getModelId: () => Promise<string>;
private kbService?: KnowledgeBaseService;
private enableKnowledgeBase: boolean;
private readonly registrations: RegistrationCallback[] = [];
@ -78,36 +79,40 @@ export class ObservabilityAIAssistantService {
core,
taskManager,
getModelId,
enableKnowledgeBase,
}: {
logger: Logger;
core: CoreSetup<ObservabilityAIAssistantPluginStartDependencies>;
taskManager: TaskManagerSetupContract;
getModelId: () => Promise<string>;
enableKnowledgeBase: boolean;
}) {
this.core = core;
this.logger = logger;
this.getModelId = getModelId;
this.enableKnowledgeBase = enableKnowledgeBase;
this.allowInit();
taskManager.registerTaskDefinitions({
[INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: {
title: 'Index queued KB articles',
description:
'Indexes previously registered entries into the knowledge base when it is ready',
timeout: '30m',
maxAttempts: 2,
createTaskRunner: (context) => {
return {
run: async () => {
if (this.kbService) {
await this.kbService.processQueue();
}
},
};
if (enableKnowledgeBase) {
taskManager.registerTaskDefinitions({
[INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: {
title: 'Index queued KB articles',
description:
'Indexes previously registered entries into the knowledge base when it is ready',
timeout: '30m',
maxAttempts: 2,
createTaskRunner: (context) => {
return {
run: async () => {
if (this.kbService) {
await this.kbService.processQueue();
}
},
};
},
},
},
});
});
}
}
getKnowledgeBaseStatus() {
@ -237,6 +242,7 @@ export class ObservabilityAIAssistantService {
esClient,
taskManagerStart: pluginsStart.taskManager,
getModelId: this.getModelId,
enabled: this.enableKnowledgeBase,
});
this.logger.info('Successfully set up index assets');
@ -331,58 +337,62 @@ export class ObservabilityAIAssistantService {
}
addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void {
this.init()
.then(() => {
this.kbService!.queue(
entries.flatMap((entry) => {
const entryWithSystemProperties = {
...entry,
'@timestamp': new Date().toISOString(),
doc_id: entry.id,
public: true,
confidence: 'high' as const,
type: 'contextual' as const,
is_correction: false,
labels: {
...entry.labels,
},
role: KnowledgeBaseEntryRole.Elastic,
};
if (this.enableKnowledgeBase) {
this.init()
.then(() => {
this.kbService!.queue(
entries.flatMap((entry) => {
const entryWithSystemProperties = {
...entry,
'@timestamp': new Date().toISOString(),
doc_id: entry.id,
public: true,
confidence: 'high' as const,
type: 'contextual' as const,
is_correction: false,
labels: {
...entry.labels,
},
role: KnowledgeBaseEntryRole.Elastic,
};
const operations =
'texts' in entryWithSystemProperties
? splitKbText(entryWithSystemProperties)
: [
{
type: KnowledgeBaseEntryOperationType.Index,
document: entryWithSystemProperties,
},
];
const operations =
'texts' in entryWithSystemProperties
? splitKbText(entryWithSystemProperties)
: [
{
type: KnowledgeBaseEntryOperationType.Index,
document: entryWithSystemProperties,
},
];
return operations;
})
);
})
.catch((error) => {
this.logger.error(
`Could not index ${entries.length} entries because of an initialisation error`
);
this.logger.error(error);
});
return operations;
})
);
})
.catch((error) => {
this.logger.error(
`Could not index ${entries.length} entries because of an initialisation error`
);
this.logger.error(error);
});
}
}
addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) {
this.addToKnowledgeBaseQueue(
entries.map((entry) => {
return {
...entry,
labels: {
...entry.labels,
category: categoryId,
},
};
})
);
if (this.enableKnowledgeBase) {
this.addToKnowledgeBaseQueue(
entries.map((entry) => {
return {
...entry,
labels: {
...entry.labels,
category: categoryId,
},
};
})
);
}
}
register(cb: RegistrationCallback) {

View file

@ -34,6 +34,7 @@ interface Dependencies {
logger: Logger;
taskManagerStart: TaskManagerStartContract;
getModelId: () => Promise<string>;
enabled: boolean;
}
export interface RecalledEntry {
@ -92,6 +93,9 @@ export class KnowledgeBaseService {
}
setup = async () => {
if (!this.dependencies.enabled) {
return;
}
const elserModelId = await this.dependencies.getModelId();
const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 };
@ -113,9 +117,9 @@ export class KnowledgeBaseService {
} catch (error) {
if (isModelMissingOrUnavailableError(error)) {
return false;
} else {
throw error;
}
throw error;
}
};
@ -202,6 +206,9 @@ export class KnowledgeBaseService {
};
private ensureTaskScheduled() {
if (!this.dependencies.enabled) {
return;
}
this.dependencies.taskManagerStart
.ensureScheduled({
taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE,
@ -251,7 +258,7 @@ export class KnowledgeBaseService {
}
async processQueue() {
if (!this._queue.length) {
if (!this._queue.length || !this.dependencies.enabled) {
return;
}
@ -305,6 +312,9 @@ export class KnowledgeBaseService {
}
status = async () => {
if (!this.dependencies.enabled) {
return { ready: false, enabled: false };
}
const elserModelId = await this.dependencies.getModelId();
try {
@ -320,11 +330,13 @@ export class KnowledgeBaseService {
deployment_state: deploymentState,
allocation_state: allocationState,
model_name: elserModelId,
enabled: true,
};
} catch (error) {
return {
error: error instanceof errors.ResponseError ? error.body.error : String(error),
ready: false,
enabled: true,
model_name: elserModelId,
};
}
@ -402,6 +414,9 @@ export class KnowledgeBaseService {
}): Promise<{
entries: RecalledEntry[];
}> => {
if (!this.dependencies.enabled) {
return { entries: [] };
}
this.dependencies.logger.debug(
() => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"`
);
@ -474,6 +489,9 @@ export class KnowledgeBaseService {
namespace: string,
user?: { name: string }
): Promise<Array<Instruction & { public?: boolean }>> => {
if (!this.dependencies.enabled) {
return [];
}
try {
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
index: resourceNames.aliases.kb,
@ -514,6 +532,9 @@ export class KnowledgeBaseService {
sortBy?: string;
sortDirection?: 'asc' | 'desc';
}): Promise<{ entries: KnowledgeBaseEntry[] }> => {
if (!this.dependencies.enabled) {
return { entries: [] };
}
try {
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
index: resourceNames.aliases.kb,
@ -578,6 +599,9 @@ export class KnowledgeBaseService {
user?: { name: string; id?: string };
namespace?: string;
}) => {
if (!this.dependencies.enabled) {
return null;
}
const res = await this.dependencies.esClient.asInternalUser.search<
Pick<KnowledgeBaseEntry, 'doc_id'>
>({
@ -607,6 +631,9 @@ export class KnowledgeBaseService {
user?: { name: string; id?: string };
namespace?: string;
}): Promise<void> => {
if (!this.dependencies.enabled) {
return;
}
// for now we want to limit the number of user instructions to 1 per user
if (document.type === KnowledgeBaseType.UserInstruction) {
const existingId = await this.getExistingUserInstructionId({
@ -647,6 +674,9 @@ export class KnowledgeBaseService {
}: {
operations: KnowledgeBaseEntryOperation[];
}): Promise<void> => {
if (!this.dependencies.enabled) {
return;
}
this.dependencies.logger.info(`Starting import of ${operations.length} entries`);
const limiter = pLimit(5);

View file

@ -164,7 +164,7 @@ export function NavControl() {
onClose={() => {
setIsOpen(false);
}}
navigateToConversation={(conversationId: string) => {
navigateToConversation={(conversationId?: string) => {
application.navigateToUrl(
http.basePath.prepend(
`/app/observabilityAIAssistant/conversations/${conversationId || ''}`

View file

@ -6,9 +6,24 @@
"id": "observabilityAiAssistantManagement",
"server": true,
"browser": true,
"configPath": ["xpack", "observabilityAiAssistantManagement"],
"requiredPlugins": ["management", "observabilityAIAssistant", "observabilityShared"],
"optionalPlugins": ["actions", "home", "serverless", "enterpriseSearch"],
"requiredBundles": ["kibanaReact", "logsDataAccess"]
"configPath": [
"xpack",
"observabilityAiAssistantManagement"
],
"requiredPlugins": [
"actions",
"management",
"observabilityAIAssistant",
"observabilityShared"
],
"optionalPlugins": [
"home",
"serverless",
"enterpriseSearch"
],
"requiredBundles": [
"kibanaReact",
"logsDataAccess",
]
}
}

View file

@ -15,7 +15,11 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup } from '@kbn/core/public';
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { StartDependencies, AiAssistantManagementObservabilityPluginStart } from './plugin';
import {
StartDependencies,
AiAssistantManagementObservabilityPluginStart,
ConfigSchema,
} from './plugin';
import { aIAssistantManagementObservabilityRouter } from './routes/config';
import { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized';
import { AppContextProvider } from './context/app_context';
@ -23,9 +27,10 @@ import { AppContextProvider } from './context/app_context';
interface MountParams {
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>;
mountParams: ManagementAppMountParams;
config: ConfigSchema;
}
export const mountManagementSection = async ({ core, mountParams }: MountParams) => {
export const mountManagementSection = async ({ core, mountParams, config }: MountParams) => {
const [coreStart, startDeps] = await core.getStartServices();
if (!startDeps.observabilityAIAssistant) return () => {};
@ -46,7 +51,7 @@ export const mountManagementSection = async ({ core, mountParams }: MountParams)
<RedirectToHomeIfUnauthorized coreStart={coreStart}>
<I18nProvider>
<KibanaContextProvider services={{ ...coreStart, ...startDeps }}>
<AppContextProvider value={{ setBreadcrumbs }}>
<AppContextProvider value={{ setBreadcrumbs, config }}>
<QueryClientProvider client={queryClient}>
<RouterProvider
history={history}

View file

@ -7,9 +7,11 @@
import React, { createContext } from 'react';
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
import { ConfigSchema } from '../plugin';
export interface AppContextValue {
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
config: ConfigSchema;
}
export const AppContext = createContext<AppContextValue>(null as any);

View file

@ -70,6 +70,11 @@ export const render = (
const appContextValue = mocks?.appContextValue ?? {
setBreadcrumbs: () => {},
config: {
logSourcesEnabled: true,
spacesEnabled: true,
visibilityEnabled: true,
},
};
return testLibRender(

View file

@ -5,13 +5,26 @@
* 2.0.
*/
import { AiAssistantManagementObservabilityPlugin as AiAssistantManagementObservabilityPlugin } from './plugin';
import { PluginInitializer, PluginInitializerContext } from '@kbn/core-plugins-browser';
import {
AiAssistantManagementObservabilityPlugin,
AiAssistantManagementObservabilityPluginSetup,
AiAssistantManagementObservabilityPluginStart,
ConfigSchema,
SetupDependencies,
StartDependencies,
} from './plugin';
export type {
AiAssistantManagementObservabilityPluginSetup,
AiAssistantManagementObservabilityPluginStart,
} from './plugin';
export function plugin() {
return new AiAssistantManagementObservabilityPlugin();
}
export const plugin: PluginInitializer<
AiAssistantManagementObservabilityPluginSetup,
AiAssistantManagementObservabilityPluginStart,
SetupDependencies,
StartDependencies
> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) => {
return new AiAssistantManagementObservabilityPlugin(pluginInitializerContext);
};

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { CoreSetup, Plugin } from '@kbn/core/public';
import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public';
import { ManagementSetup } from '@kbn/management-plugin/public';
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { ServerlessPluginStart } from '@kbn/serverless/public';
@ -35,6 +35,12 @@ export interface StartDependencies {
enterpriseSearch?: EnterpriseSearchPublicStart;
}
export interface ConfigSchema {
logSourcesEnabled: boolean;
spacesEnabled: boolean;
visibilityEnabled: boolean;
}
export class AiAssistantManagementObservabilityPlugin
implements
Plugin<
@ -44,6 +50,12 @@ export class AiAssistantManagementObservabilityPlugin
StartDependencies
>
{
private readonly config: ConfigSchema;
constructor(context: PluginInitializerContext<ConfigSchema>) {
this.config = context.config.get();
}
public setup(
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>,
{ home, management, observabilityAIAssistant }: SetupDependencies
@ -78,6 +90,7 @@ export class AiAssistantManagementObservabilityPlugin
return mountManagementSection({
core,
mountParams,
config: this.config,
});
},
});

View file

@ -8,8 +8,24 @@
import React from 'react';
import { coreStartMock, render } from '../../helpers/test_helper';
import { SettingsPage } from './settings_page';
import { useKnowledgeBase } from '@kbn/ai-assistant';
jest.mock('@kbn/ai-assistant');
const useKnowledgeBaseMock = useKnowledgeBase as jest.Mock;
describe('Settings Page', () => {
const appContextValue = {
config: { spacesEnabled: true, visibilityEnabled: true, logSourcesEnabled: true },
setBreadcrumbs: () => {},
};
useKnowledgeBaseMock.mockReturnValue({
status: {
value: {
enabled: true,
},
},
});
it('should navigate to home when not authorized', () => {
render(<SettingsPage />, {
coreStart: {
@ -21,13 +37,16 @@ describe('Settings Page', () => {
},
},
},
appContextValue,
});
expect(coreStartMock.application.navigateToApp).toBeCalledWith('home');
});
it('should render settings and knowledge base tabs', () => {
const { getByTestId } = render(<SettingsPage />);
const { getByTestId } = render(<SettingsPage />, {
appContextValue,
});
expect(getByTestId('settingsPageTab-settings')).toBeInTheDocument();
expect(getByTestId('settingsPageTab-knowledge_base')).toBeInTheDocument();
@ -36,7 +55,7 @@ describe('Settings Page', () => {
it('should set breadcrumbs', () => {
const setBreadcrumbs = jest.fn();
render(<SettingsPage />, {
appContextValue: { setBreadcrumbs },
appContextValue: { ...appContextValue, setBreadcrumbs },
});
expect(setBreadcrumbs).toHaveBeenCalledWith([

View file

@ -8,6 +8,7 @@
import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
import { useKnowledgeBase } from '@kbn/ai-assistant';
import { useAppContext } from '../../hooks/use_app_context';
import { SettingsTab } from './settings_tab/settings_tab';
import { KnowledgeBaseTab } from './knowledge_base_tab';
@ -28,6 +29,7 @@ export function SettingsPage() {
} = useKibana();
const router = useObservabilityAIAssistantManagementRouter();
const knowledgeBase = useKnowledgeBase();
const {
query: { tab },
@ -85,6 +87,7 @@ export function SettingsPage() {
}
),
content: <KnowledgeBaseTab />,
disabled: !knowledgeBase.status.value?.enabled,
},
{
id: 'search_connector',

View file

@ -9,10 +9,14 @@ import React from 'react';
import { fireEvent } from '@testing-library/react';
import { render } from '../../../helpers/test_helper';
import { SettingsTab } from './settings_tab';
import { useAppContext } from '../../../hooks/use_app_context';
jest.mock('../../../hooks/use_app_context');
const useAppContextMock = useAppContext as jest.Mock;
describe('SettingsTab', () => {
useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } });
it('should offer a way to configure Observability AI Assistant visibility in apps', () => {
const navigateToAppMock = jest.fn(() => Promise.resolve());
const { getByTestId } = render(<SettingsTab />, {

View file

@ -8,6 +8,7 @@
import React from 'react';
import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useAppContext } from '../../../hooks/use_app_context';
import { useKibana } from '../../../hooks/use_kibana';
import { UISettings } from './ui_settings';
@ -15,6 +16,7 @@ export function SettingsTab() {
const {
application: { navigateToApp },
} = useKibana().services;
const { config } = useAppContext();
const handleNavigateToConnectors = () => {
navigateToApp('management', {
@ -30,44 +32,46 @@ export function SettingsTab() {
return (
<EuiPanel hasBorder grow={false}>
<EuiDescribedFormGroup
fullWidth
title={
<h3>
{i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel',
{
defaultMessage:
'Show AI Assistant button and Contextual Insights in Observability apps',
}
)}
</h3>
}
description={
<p>
{i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel',
{
defaultMessage:
'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > <your space> > Features.',
ignoreTag: true,
}
)}
</p>
}
>
<EuiFormRow fullWidth>
<EuiButton
data-test-subj="settingsTabGoToSpacesButton"
onClick={handleNavigateToSpacesConfiguration}
>
{i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
{ defaultMessage: 'Go to Spaces' }
)}
</EuiButton>
</EuiFormRow>
</EuiDescribedFormGroup>
{config.spacesEnabled && (
<EuiDescribedFormGroup
fullWidth
title={
<h3>
{i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel',
{
defaultMessage:
'Show AI Assistant button and Contextual Insights in Observability apps',
}
)}
</h3>
}
description={
<p>
{i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel',
{
defaultMessage:
'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > <your space> > Features.',
ignoreTag: true,
}
)}
</p>
}
>
<EuiFormRow fullWidth>
<EuiButton
data-test-subj="settingsTabGoToSpacesButton"
onClick={handleNavigateToSpacesConfiguration}
>
{i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
{ defaultMessage: 'Go to Spaces' }
)}
</EuiButton>
</EuiFormRow>
</EuiDescribedFormGroup>
)}
<EuiDescribedFormGroup
fullWidth

View file

@ -18,14 +18,10 @@ import { EuiSpacer } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
import { LogSourcesSettingSynchronisationInfo } from '@kbn/logs-data-access-plugin/public';
import { useKnowledgeBase } from '@kbn/ai-assistant';
import { useAppContext } from '../../../hooks/use_app_context';
import { useKibana } from '../../../hooks/use_kibana';
const settingsKeys = [
aiAssistantSimulatedFunctionCalling,
aiAssistantSearchConnectorIndexPattern,
aiAssistantPreferredAIAssistantType,
];
export function UISettings() {
const {
docLinks,
@ -33,6 +29,14 @@ export function UISettings() {
notifications,
application: { capabilities, getUrlForApp },
} = useKibana().services;
const knowledgeBase = useKnowledgeBase();
const { config } = useAppContext();
const settingsKeys = [
aiAssistantSimulatedFunctionCalling,
...(knowledgeBase.status.value?.enabled ? [aiAssistantSearchConnectorIndexPattern] : []),
...(config.visibilityEnabled ? [aiAssistantPreferredAIAssistantType] : []),
];
const { fields, handleFieldChange, unsavedChanges, saveAll, isSaving, cleanUnsavedChanges } =
useEditableSettings(settingsKeys);
@ -84,12 +88,13 @@ export function UISettings() {
</FieldRowProvider>
);
})}
<LogSourcesSettingSynchronisationInfo
isLoading={false}
logSourcesValue={settings.client.get(aiAssistantLogsIndexPattern)}
getUrlForApp={getUrlForApp}
/>
{config.logSourcesEnabled && (
<LogSourcesSettingSynchronisationInfo
isLoading={false}
logSourcesValue={settings.client.get(aiAssistantLogsIndexPattern)}
getUrlForApp={getUrlForApp}
/>
)}
{!isEmpty(unsavedChanges) && (
<BottomBarActions

View file

@ -0,0 +1,22 @@
/*
* 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 { schema, type TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor } from '@kbn/core-plugins-server';
const configSchema = schema.object({
visibilityEnabled: schema.boolean({ defaultValue: true }),
spacesEnabled: schema.boolean({ defaultValue: true }),
logSourcesEnabled: schema.boolean({ defaultValue: true }),
});
export type ObservabilityAIAssistantManagementConfig = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<ObservabilityAIAssistantManagementConfig> = {
schema: configSchema,
exposeToBrowser: { logSourcesEnabled: true, spacesEnabled: true, visibilityEnabled: true },
};

View file

@ -5,6 +5,8 @@
* 2.0.
*/
export { config } from './config';
export const plugin = async () => {
const { AiAssistantManagementPlugin } = await import('./plugin');
return new AiAssistantManagementPlugin();

View file

@ -3,7 +3,11 @@
"compilerOptions": {
"outDir": "target/types"
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"include": [
"common/**/*",
"public/**/*",
"server/**/*"
],
"kbn_references": [
"@kbn/core",
"@kbn/home-plugin",
@ -22,6 +26,11 @@
"@kbn/config-schema",
"@kbn/core-ui-settings-common",
"@kbn/logs-data-access-plugin",
"@kbn/core-plugins-browser",
"@kbn/ai-assistant",
"@kbn/core-plugins-server"
],
"exclude": ["target/**/*"]
"exclude": [
"target/**/*"
]
}

View file

@ -15,7 +15,6 @@
"actions",
"licensing",
"observabilityAIAssistant",
"observabilityAIAssistantApp",
"triggersActionsUi",
"share"
],

View file

@ -1,34 +0,0 @@
/*
* 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 ReactDOM from 'react-dom';
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider } from '@kbn/i18n-react';
import type { SearchAssistantPluginStartDependencies } from './types';
import { SearchAssistantRouter } from './components/routes/router';
export const renderApp = (
core: CoreStart,
services: SearchAssistantPluginStartDependencies,
appMountParameters: AppMountParameters
) => {
ReactDOM.render(
<KibanaRenderContextProvider {...core}>
<KibanaContextProvider services={{ ...core, ...services }}>
<I18nProvider>
<SearchAssistantRouter history={appMountParameters.history} />
</I18nProvider>
</KibanaContextProvider>
</KibanaRenderContextProvider>,
appMountParameters.element
);
return () => ReactDOM.unmountComponentAtNode(appMountParameters.element);
};

View file

@ -1,15 +0,0 @@
/*
* 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 { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
export const App: React.FC = () => {
return (
<KibanaPageTemplate.Section alignment="top" restrictWidth={false} grow paddingSize="none">
<div />
</KibanaPageTemplate.Section>
);
};

View file

@ -0,0 +1,152 @@
/*
* 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, useRef, useState } from 'react';
import { AssistantAvatar, useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public';
import { EuiButton, EuiLoadingSpinner, EuiToolTip, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import { v4 } from 'uuid';
import useObservable from 'react-use/lib/useObservable';
import { i18n } from '@kbn/i18n';
import { useAIAssistantAppService, ChatFlyout } from '@kbn/ai-assistant';
import { useKibana } from '@kbn/ai-assistant/src/hooks/use_kibana';
import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types';
import { EuiErrorBoundary } from '@elastic/eui';
import type { CoreStart } from '@kbn/core/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
interface NavControlWithProviderDeps {
coreStart: CoreStart;
pluginsStart: AIAssistantPluginStartDependencies;
}
export const NavControlWithProvider = ({ coreStart, pluginsStart }: NavControlWithProviderDeps) => {
return (
<EuiErrorBoundary>
<KibanaThemeProvider theme={coreStart.theme}>
<KibanaContextProvider
services={{
...coreStart,
...pluginsStart,
}}
>
<RedirectAppLinks coreStart={coreStart}>
<coreStart.i18n.Context>
<NavControl />
</coreStart.i18n.Context>
</RedirectAppLinks>
</KibanaContextProvider>
</KibanaThemeProvider>
</EuiErrorBoundary>
);
};
export function NavControl() {
const service = useAIAssistantAppService();
const {
services: { notifications, observabilityAIAssistant },
} = useKibana();
const [hasBeenOpened, setHasBeenOpened] = useState(false);
const chatService = useAbortableAsync(
({ signal }) => {
return hasBeenOpened
? service.start({ signal }).catch((error) => {
notifications?.toasts.addError(error, {
title: i18n.translate('xpack.searchAssistant.navControl.initFailureErrorTitle', {
defaultMessage: 'Failed to initialize AI Assistant',
}),
});
setHasBeenOpened(false);
setIsOpen(false);
throw error;
})
: undefined;
},
[service, hasBeenOpened, notifications?.toasts]
);
const [isOpen, setIsOpen] = useState(false);
const keyRef = useRef(v4());
useEffect(() => {
const conversationSubscription = service.conversations.predefinedConversation$.subscribe(() => {
keyRef.current = v4();
setHasBeenOpened(true);
setIsOpen(true);
});
return () => {
conversationSubscription.unsubscribe();
};
}, [service.conversations.predefinedConversation$]);
const { messages, title } = useObservable(service.conversations.predefinedConversation$) ?? {
messages: [],
title: undefined,
};
const theme = useEuiTheme().euiTheme;
const buttonCss = css`
padding: 0px 8px;
svg path {
fill: ${theme.colors.darkestShade};
}
`;
return (
<>
<EuiToolTip content={buttonLabel}>
<EuiButton
aria-label={buttonLabel}
data-test-subj="AiAssistantAppNavControlButton"
css={buttonCss}
onClick={() => {
service.conversations.openNewConversation({
messages: [],
});
}}
color="primary"
size="s"
fullWidth={false}
minWidth={0}
>
{chatService.loading ? <EuiLoadingSpinner size="s" /> : <AssistantAvatar size="xs" />}
</EuiButton>
</EuiToolTip>
{chatService.value &&
Boolean(observabilityAIAssistant?.ObservabilityAIAssistantChatServiceContext) ? (
<observabilityAIAssistant.ObservabilityAIAssistantChatServiceContext.Provider
value={chatService.value}
>
<ChatFlyout
key={keyRef.current}
isOpen={isOpen}
initialMessages={messages}
initialTitle={title ?? ''}
onClose={() => {
setIsOpen(false);
}}
/>
</observabilityAIAssistant.ObservabilityAIAssistantChatServiceContext.Provider>
) : undefined}
</>
);
}
const buttonLabel = i18n.translate(
'xpack.searchAssistant.navControl.openTheAIAssistantPopoverLabel',
{ defaultMessage: 'Open the AI Assistant' }
);

View file

@ -0,0 +1,26 @@
/*
* 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 { dynamic } from '@kbn/shared-ux-utility';
import React from 'react';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { AIAssistantAppService } from '@kbn/ai-assistant';
import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types';
const LazyNavControlWithProvider = dynamic(() =>
import('.').then((m) => ({ default: m.NavControlWithProvider }))
);
interface NavControlInitiatorProps {
appService: AIAssistantAppService;
coreStart: CoreStart;
pluginsStart: AIAssistantPluginStartDependencies;
}
export const NavControlInitiator = ({ coreStart, pluginsStart }: NavControlInitiatorProps) => {
return <LazyNavControlWithProvider coreStart={coreStart} pluginsStart={pluginsStart} />;
};

View file

@ -1,36 +0,0 @@
/*
* 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 { ConversationView } from '@kbn/ai-assistant';
import { useParams } from 'react-router-dom';
import { useKibana } from '@kbn/kibana-react-plugin/public';
export function ConversationViewWithProps() {
const { conversationId } = useParams<{ conversationId?: string }>();
const {
services: { application, http },
} = useKibana();
function navigateToConversation(nextConversationId?: string) {
application?.navigateToUrl(
http?.basePath.prepend(`/app/searchAssistant/conversations/${nextConversationId || ''}`) || ''
);
}
return (
<ConversationView
conversationId={conversationId}
navigateToConversation={navigateToConversation}
newConversationHref={
http?.basePath.prepend(`/app/searchAssistant/conversations/new|| ''}`) || ''
}
getConversationHref={(id: string) =>
http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || ''
}
scopes={['search']}
/>
);
}

View file

@ -1,32 +0,0 @@
/*
* 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 { History } from 'history';
import { Route, Router, Routes } from '@kbn/shared-ux-router';
import { Redirect } from 'react-router-dom';
import { SearchAIAssistantPageTemplate } from '../page_template';
import { ConversationViewWithProps } from './conversations/conversation_view_with_props';
export const SearchAssistantRouter: React.FC<{ history: History }> = ({ history }) => {
return (
<SearchAIAssistantPageTemplate>
<Router history={history}>
<Routes>
<Redirect from="/" to="/conversations/new" exact />
<Redirect from="/conversations" to="/conversations/new" exact />
<Route path="/conversations/new" exact>
<ConversationViewWithProps />
</Route>
<Route path="/conversations/:conversationId">
<ConversationViewWithProps />
</Route>
</Routes>
</Router>
</SearchAIAssistantPageTemplate>
);
};

View file

@ -5,20 +5,16 @@
* 2.0.
*/
import {
DEFAULT_APP_CATEGORIES,
type CoreSetup,
type Plugin,
CoreStart,
AppMountParameters,
PluginInitializerContext,
} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public';
import { createAppService } from '@kbn/ai-assistant';
import ReactDOM from 'react-dom';
import React from 'react';
import type {
SearchAssistantPluginSetup,
SearchAssistantPluginStart,
SearchAssistantPluginStartDependencies,
} from './types';
import { NavControlInitiator } from './components/nav_control/lazy_nav_control';
export interface PublicConfigType {
ui: {
@ -44,36 +40,43 @@ export class SearchAssistantPlugin
public setup(
core: CoreSetup<SearchAssistantPluginStartDependencies, SearchAssistantPluginStart>
): SearchAssistantPluginSetup {
if (!this.config.ui.enabled) {
return {};
}
core.application.register({
id: 'searchAssistant',
title: i18n.translate('xpack.searchAssistant.appTitle', {
defaultMessage: 'Search Assistant',
}),
euiIconType: 'logoEnterpriseSearch',
appRoute: '/app/searchAssistant',
category: DEFAULT_APP_CATEGORIES.search,
visibleIn: [],
deepLinks: [],
mount: async (appMountParameters: AppMountParameters<unknown>) => {
// Load application bundle and Get start services
const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([
import('./application'),
core.getStartServices() as Promise<
[CoreStart, SearchAssistantPluginStartDependencies, unknown]
>,
]);
return renderApp(coreStart, pluginsStart, appMountParameters);
},
});
return {};
}
public start(): SearchAssistantPluginStart {
public start(
coreStart: CoreStart,
pluginsStart: SearchAssistantPluginStartDependencies
): SearchAssistantPluginStart {
if (!this.config.ui.enabled) {
return {};
}
const appService = createAppService({
pluginsStart,
});
const isEnabled = appService.isEnabled();
if (!isEnabled) {
return {};
}
coreStart.chrome.navControls.registerRight({
mount: (element) => {
ReactDOM.render(
<NavControlInitiator
appService={appService}
coreStart={coreStart}
pluginsStart={pluginsStart}
/>,
element,
() => {}
);
return () => {};
},
// right before the user profile
order: 1001,
});
return {};
}

View file

@ -7,6 +7,10 @@
import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { MlPluginStart } from '@kbn/ml-plugin/public';
import { SharePluginStart } from '@kbn/share-plugin/public';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SearchAssistantPluginSetup {}
@ -15,6 +19,10 @@ export interface SearchAssistantPluginSetup {}
export interface SearchAssistantPluginStart {}
export interface SearchAssistantPluginStartDependencies {
licensing: LicensingPluginStart;
ml: MlPluginStart;
observabilityAIAssistant: ObservabilityAIAssistantPublicStart;
share: SharePluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
usageCollection?: UsageCollectionStart;
}

View file

@ -13,17 +13,22 @@
],
"kbn_references": [
"@kbn/core",
"@kbn/react-kibana-context-render",
"@kbn/kibana-react-plugin",
"@kbn/i18n-react",
"@kbn/shared-ux-page-kibana-template",
"@kbn/usage-collection-plugin",
"@kbn/observability-ai-assistant-plugin",
"@kbn/config-schema",
"@kbn/ai-assistant",
"@kbn/i18n",
"@kbn/shared-ux-router",
"@kbn/serverless"
"@kbn/serverless",
"@kbn/react-kibana-context-theme",
"@kbn/shared-ux-link-redirect-app",
"@kbn/shared-ux-utility",
"@kbn/core-lifecycle-browser",
"@kbn/licensing-plugin",
"@kbn/ml-plugin",
"@kbn/share-plugin",
"@kbn/triggers-actions-ui-plugin"
],
"exclude": [
"target/**/*",

View file

@ -13,12 +13,12 @@
"requiredPlugins": [
"actions",
"features",
"ml",
"share",
],
"optionalPlugins": [
"cloud",
"console",
"ml"
],
"requiredBundles": [
"kibanaReact"

View file

@ -49,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(res.body).to.eql({
ready: false,
model_name: TINY_ELSER.id,
enabled: true,
});
});
});

View file

@ -69,6 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(res.body).to.eql({
ready: false,
model_name: TINY_ELSER.id,
enabled: true,
});
});
});

View file

@ -64,5 +64,8 @@ export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) {
keyName
);
},
async expectAIAssistantToExist() {
await testSubjects.existOrFail('AiAssistantAppNavControlButton');
},
};
}

View file

@ -84,5 +84,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await pageObjects.svlSearchHomePage.createApiKeyInFlyout('ftr-test-key');
await pageObjects.svlSearchHomePage.closeConnectionDetailsFlyout();
});
it('shows the AI assistant', async () => {
await pageObjects.svlSearchHomePage.expectAIAssistantToExist();
});
});
}