[Obs AI Assistant] move rule connector to observability_ai_assistant_app (#180949)

## Summary

Resolves https://github.com/elastic/kibana/issues/180910

This change simply moves the rule_connector introduced in
https://github.com/elastic/kibana/pull/179980 to the
`observability_ai_assistant_app` plugin. There are not functional
changes.
Also added some unit tests

### Testing
See testing section in https://github.com/elastic/kibana/pull/179980

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kevin Lacabane 2024-04-22 15:01:24 +02:00 committed by GitHub
parent f10463a979
commit e8fe9dbd1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 327 additions and 82 deletions

View file

@ -29,6 +29,6 @@ export const connectorTypes: string[] = [
'.bedrock',
'.d3security',
'.sentinelone',
'.observability-ai-assistant',
'.cases',
'.observability-ai-assistant',
];

View file

@ -14,9 +14,6 @@
"security",
"taskManager",
"dataViews",
"triggersActionsUi",
"ruleRegistry",
"alerting"
],
"requiredBundles": ["kibanaReact", "kibanaUtils"],
"optionalPlugins": ["cloud", "serverless"],

View file

@ -31,6 +31,7 @@ export type {
export { AssistantAvatar } from './components/assistant_avatar';
export { ConnectorSelectorBase } from './components/connector_selector/connector_selector_base';
export { useAbortableAsync, type AbortableAsyncState } from './hooks/use_abortable_async';
export { useGenAIConnectorsWithoutContext } from './hooks/use_genai_connectors';
export { createStorybookChatService, createStorybookService } from './storybook_mock';

View file

@ -29,7 +29,6 @@ import type {
ObservabilityAIAssistantPublicStart,
ObservabilityAIAssistantService,
} from './types';
import { getObsAIAssistantConnectorType } from './rule_connector';
export class ObservabilityAIAssistantPlugin
implements
@ -88,10 +87,6 @@ export class ObservabilityAIAssistantPlugin
const isEnabled = service.isEnabled();
pluginsStart.triggersActionsUi.actionTypeRegistry.register(
getObsAIAssistantConnectorType(service)
);
return {
service,
useGenAIConnectors: () => useGenAIConnectorsWithoutContext(service),

View file

@ -8,7 +8,6 @@
import {
CoreSetup,
DEFAULT_APP_CATEGORIES,
KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
@ -24,10 +23,7 @@ import { firstValueFrom } from 'rxjs';
import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '../common/feature';
import type { ObservabilityAIAssistantConfig } from './config';
import { registerServerRoutes } from './routes/register_routes';
import {
ObservabilityAIAssistantRequestHandlerContext,
ObservabilityAIAssistantRouteHandlerResources,
} from './routes/types';
import { ObservabilityAIAssistantRouteHandlerResources } from './routes/types';
import { ObservabilityAIAssistantService } from './service';
import {
ObservabilityAIAssistantServerSetup,
@ -38,10 +34,6 @@ import {
import { addLensDocsToKb } from './service/knowledge_base_service/kb_docs/lens';
import { registerFunctions } from './functions';
import { recallRankingEvent } from './analytics/recall_ranking';
import {
getObsAIAssistantConnectorType,
getObsAIAssistantConnectorAdapter,
} from './rule_connector';
export class ObservabilityAIAssistantPlugin
implements
@ -162,55 +154,6 @@ export class ObservabilityAIAssistantPlugin
addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') });
const initResources = async (
request: KibanaRequest
): Promise<ObservabilityAIAssistantRouteHandlerResources> => {
const [coreStart, pluginsStart] = await core.getStartServices();
const license = await firstValueFrom(pluginsStart.licensing.license$);
const savedObjectsClient = coreStart.savedObjects.getScopedClient(request);
const context: ObservabilityAIAssistantRequestHandlerContext = {
rac: routeHandlerPlugins.ruleRegistry.start().then((startContract) => {
return {
getAlertsClient() {
return startContract.getRacClientWithRequest(request);
},
};
}),
alerting: routeHandlerPlugins.alerting.start().then((startContract) => {
return {
getRulesClient() {
return startContract.getRulesClientWithRequest(request);
},
};
}),
core: Promise.resolve({
coreStart,
elasticsearch: {
client: coreStart.elasticsearch.client.asScoped(request),
},
uiSettings: {
client: coreStart.uiSettings.asScopedToClient(savedObjectsClient),
},
savedObjects: {
client: savedObjectsClient,
},
}),
licensing: Promise.resolve({ license, featureUsage: pluginsStart.licensing.featureUsage }),
};
return {
request,
service,
context,
logger: this.logger.get('connector'),
plugins: routeHandlerPlugins,
};
};
plugins.actions.registerType(getObsAIAssistantConnectorType(initResources));
plugins.alerting.registerConnectorAdapter(getObsAIAssistantConnectorAdapter());
registerServerRoutes({
core,
logger: this.logger,

View file

@ -48,7 +48,6 @@
"@kbn/cloud-plugin",
"@kbn/serverless",
"@kbn/triggers-actions-ui-plugin",
"@kbn/stack-connectors-plugin",
],
"exclude": ["target/**/*"]
}

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 const OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID = '.observability-ai-assistant';

View file

@ -21,7 +21,9 @@
"share",
"security",
"licensing",
"ml"
"ml",
"alerting",
"features"
],
"requiredBundles": ["kibanaReact"],
"optionalPlugins": ["cloud"],

View file

@ -26,6 +26,7 @@ import type {
import { createAppService, ObservabilityAIAssistantAppService } from './service/create_app_service';
import { SharedProviders } from './utils/shared_providers';
import { LazyNavControl } from './components/nav_control/lazy_nav_control';
import { getObsAIAssistantConnectorType } from './rule_connector';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ConfigSchema {}
@ -124,12 +125,17 @@ export class ObservabilityAIAssistantAppPlugin
order: 1001,
});
pluginsStart.observabilityAIAssistant.service.register(async ({ registerRenderFunction }) => {
const service = pluginsStart.observabilityAIAssistant.service;
service.register(async ({ registerRenderFunction }) => {
const { registerFunctions } = await import('./functions');
await registerFunctions({ pluginsStart, registerRenderFunction });
});
pluginsStart.triggersActionsUi.actionTypeRegistry.register(
getObsAIAssistantConnectorType(service)
);
return {};
}
}

View file

@ -10,10 +10,12 @@ import type {
ActionTypeModel as ConnectorTypeModel,
GenericValidationResult,
} from '@kbn/triggers-actions-ui-plugin/public/types';
import { ObsAIAssistantActionParams } from './types';
import { ObservabilityAIAssistantService } from '../types';
import { AssistantAvatar } from '../components/assistant_avatar';
import {
AssistantAvatar,
ObservabilityAIAssistantService,
} from '@kbn/observability-ai-assistant-plugin/public';
import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector';
import { ObsAIAssistantActionParams } from './types';
import {
CONNECTOR_DESC,
CONNECTOR_REQUIRED,

View file

@ -9,9 +9,11 @@ import React, { useEffect } from 'react';
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiFlexItem, EuiSelect, EuiSpacer, EuiTextArea } from '@elastic/eui';
import {
ObservabilityAIAssistantService,
useGenAIConnectorsWithoutContext,
} from '@kbn/observability-ai-assistant-plugin/public';
import { ObsAIAssistantActionParams } from './types';
import { ObservabilityAIAssistantService } from '../types';
import { useGenAIConnectorsWithoutContext } from '../hooks/use_genai_connectors';
const ObsAIAssistantParamsFields: React.FunctionComponent<
ActionParamsProps<ObsAIAssistantActionParams> & { service: ObservabilityAIAssistantService }

View file

@ -11,9 +11,21 @@ import {
Plugin,
type PluginInitializerContext,
type CoreStart,
KibanaRequest,
} from '@kbn/core/server';
import {
ObservabilityAIAssistantRequestHandlerContext,
ObservabilityAIAssistantRouteHandlerResources,
} from '@kbn/observability-ai-assistant-plugin/server/routes/types';
import { ObservabilityAIAssistantPluginStartDependencies } from '@kbn/observability-ai-assistant-plugin/server/types';
import { mapValues } from 'lodash';
import { firstValueFrom } from 'rxjs';
import type { ObservabilityAIAssistantAppConfig } from './config';
import { registerFunctions } from './functions';
import {
getObsAIAssistantConnectorAdapter,
getObsAIAssistantConnectorType,
} from './rule_connector';
import type {
ObservabilityAIAssistantAppPluginSetupDependencies,
ObservabilityAIAssistantAppPluginStartDependencies,
@ -42,6 +54,68 @@ export class ObservabilityAIAssistantAppPlugin
>,
plugins: ObservabilityAIAssistantAppPluginSetupDependencies
): ObservabilityAIAssistantAppServerSetup {
const routeHandlerPlugins = mapValues(plugins, (value, key) => {
return {
setup: value,
start: () =>
core.getStartServices().then((services) => {
const [, pluginsStartContracts] = services;
return pluginsStartContracts[
key as keyof ObservabilityAIAssistantPluginStartDependencies
];
}),
};
}) as ObservabilityAIAssistantRouteHandlerResources['plugins'];
const initResources = async (
request: KibanaRequest
): Promise<ObservabilityAIAssistantRouteHandlerResources> => {
const [coreStart, pluginsStart] = await core.getStartServices();
const license = await firstValueFrom(pluginsStart.licensing.license$);
const savedObjectsClient = coreStart.savedObjects.getScopedClient(request);
const context: ObservabilityAIAssistantRequestHandlerContext = {
rac: routeHandlerPlugins.ruleRegistry.start().then((startContract) => {
return {
getAlertsClient() {
return startContract.getRacClientWithRequest(request);
},
};
}),
alerting: routeHandlerPlugins.alerting.start().then((startContract) => {
return {
getRulesClient() {
return startContract.getRulesClientWithRequest(request);
},
};
}),
core: Promise.resolve({
coreStart,
elasticsearch: {
client: coreStart.elasticsearch.client.asScoped(request),
},
uiSettings: {
client: coreStart.uiSettings.asScopedToClient(savedObjectsClient),
},
savedObjects: {
client: savedObjectsClient,
},
}),
licensing: Promise.resolve({ license, featureUsage: pluginsStart.licensing.featureUsage }),
};
return {
request,
context,
service: plugins.observabilityAIAssistant.service,
logger: this.logger.get('connector'),
plugins: routeHandlerPlugins,
};
};
plugins.actions.registerType(getObsAIAssistantConnectorType(initResources));
plugins.alerting.registerConnectorAdapter(getObsAIAssistantConnectorAdapter());
return {};
}

View file

@ -8,7 +8,7 @@
import joiToJsonSchema from 'joi-to-json';
import type { Type } from '@kbn/config-schema';
import { castArray, isPlainObject, forEach, unset } from 'lodash';
import type { CompatibleJSONSchema } from '../../common/functions/types';
import type { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/common/functions/types';
function dropUnknownProperties(object: CompatibleJSONSchema) {
if (!isPlainObject(object)) {

View file

@ -0,0 +1,168 @@
/*
* 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 { AlertHit } from '@kbn/alerting-plugin/server/types';
import { ObservabilityAIAssistantRouteHandlerResources } from '@kbn/observability-ai-assistant-plugin/server/routes/types';
import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request';
import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector';
import {
getObsAIAssistantConnectorAdapter,
getObsAIAssistantConnectorType,
ObsAIAssistantConnectorTypeExecutorOptions,
} from '.';
import { Observable } from 'rxjs';
import { MessageRole } from '@kbn/observability-ai-assistant-plugin/public';
describe('observabilityAIAssistant rule_connector', () => {
describe('getObsAIAssistantConnectorAdapter', () => {
it('uses correct connector_id', () => {
const adapter = getObsAIAssistantConnectorAdapter();
expect(adapter.connectorTypeId).toEqual(OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID);
});
it('builds action params', () => {
const adapter = getObsAIAssistantConnectorAdapter();
const params = adapter.buildActionParams({
params: { connector: '.azure', message: 'hello' },
rule: { id: 'foo', name: 'bar', tags: [], consumer: '', producer: '' },
ruleUrl: 'http://myrule.com',
spaceId: 'default',
alerts: {
all: { count: 1, data: [] },
new: { count: 1, data: [{ _id: 'new_alert' } as AlertHit] },
ongoing: { count: 1, data: [] },
recovered: { count: 1, data: [{ _id: 'recovered_alert' } as AlertHit] },
},
});
expect(params).toEqual({
connector: '.azure',
message: 'hello',
rule: { id: 'foo', name: 'bar', tags: [], ruleUrl: 'http://myrule.com' },
alerts: {
new: [{ _id: 'new_alert' }],
recovered: [{ _id: 'recovered_alert' }],
},
});
});
});
describe('getObsAIAssistantConnectorType', () => {
it('is correctly configured', () => {
const initResources = jest
.fn()
.mockResolvedValue({} as ObservabilityAIAssistantRouteHandlerResources);
const connectorType = getObsAIAssistantConnectorType(initResources);
expect(connectorType.id).toEqual(OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID);
expect(connectorType.isSystemActionType).toEqual(true);
expect(connectorType.minimumLicenseRequired).toEqual('enterprise');
});
it('does not execute when no new or recovered alerts', async () => {
const initResources = jest
.fn()
.mockResolvedValue({} as ObservabilityAIAssistantRouteHandlerResources);
const connectorType = getObsAIAssistantConnectorType(initResources);
const result = await connectorType.executor({
actionId: 'observability-ai-assistant',
request: getFakeKibanaRequest({ id: 'foo', api_key: 'bar' }),
params: { alerts: { new: [], recovered: [] } },
} as unknown as ObsAIAssistantConnectorTypeExecutorOptions);
expect(result).toEqual({ actionId: 'observability-ai-assistant', status: 'ok' });
expect(initResources).not.toHaveBeenCalled();
});
it('calls complete api', async () => {
const completeMock = jest.fn().mockReturnValue(new Observable());
const initResources = jest.fn().mockResolvedValue({
service: {
getClient: async () => ({ complete: completeMock }),
getFunctionClient: async () => ({
getContexts: () => [{ name: 'core', description: 'my_system_message' }],
}),
},
context: {
core: Promise.resolve({
coreStart: { http: { basePath: { publicBaseUrl: 'http://kibana.com' } } },
}),
},
plugins: {
actions: {
start: async () => {
return {
getActionsClientWithRequest: jest.fn().mockResolvedValue({
async getAll() {
return [{ id: 'connector_1' }];
},
}),
};
},
},
},
} as unknown as ObservabilityAIAssistantRouteHandlerResources);
const connectorType = getObsAIAssistantConnectorType(initResources);
const result = await connectorType.executor({
actionId: 'observability-ai-assistant',
request: getFakeKibanaRequest({ id: 'foo', api_key: 'bar' }),
params: {
message: 'hello',
connector: 'azure-open-ai',
alerts: { new: [{ _id: 'new_alert' }], recovered: [] },
},
} as unknown as ObsAIAssistantConnectorTypeExecutorOptions);
expect(result).toEqual({ actionId: 'observability-ai-assistant', status: 'ok' });
expect(initResources).toHaveBeenCalledTimes(1);
expect(completeMock).toHaveBeenCalledTimes(1);
expect(completeMock).toHaveBeenCalledWith(
expect.objectContaining({
persist: true,
isPublic: true,
connectorId: 'azure-open-ai',
kibanaPublicUrl: 'http://kibana.com',
messages: [
{
'@timestamp': expect.any(String),
message: {
role: MessageRole.System,
content: 'my_system_message',
},
},
{
'@timestamp': expect.any(String),
message: {
role: MessageRole.User,
content: 'hello',
},
},
{
'@timestamp': expect.any(String),
message: {
role: MessageRole.Assistant,
content: '',
function_call: {
name: 'get_connectors',
arguments: JSON.stringify({}),
trigger: MessageRole.Assistant as const,
},
},
},
{
'@timestamp': expect.any(String),
message: {
role: MessageRole.User,
name: 'get_connectors',
content: JSON.stringify({ connectors: [{ id: 'connector_1' }] }),
},
},
],
})
);
});
});
});

View file

@ -24,16 +24,16 @@ import {
SlackParamsSchema,
WebhookParamsSchema,
} from '@kbn/stack-connectors-plugin/server';
import { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types';
import { ObservabilityAIAssistantRouteHandlerResources } from '@kbn/observability-ai-assistant-plugin/server/routes/types';
import {
ChatCompletionChunkEvent,
MessageRole,
StreamingChatResponseEventType,
} from '../../common';
import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector';
import { concatenateChatCompletionChunks } from '../../common/utils/concatenate_chat_completion_chunks';
} from '@kbn/observability-ai-assistant-plugin/common';
import { concatenateChatCompletionChunks } from '@kbn/observability-ai-assistant-plugin/common/utils/concatenate_chat_completion_chunks';
import { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/common/functions/types';
import { convertSchemaToOpenApi } from './convert_schema_to_open_api';
import { CompatibleJSONSchema } from '../../common/functions/types';
import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector';
const CONNECTOR_PRIVILEGES = ['api:observabilityAIAssistant', 'app:observabilityAIAssistant'];

View file

@ -5,6 +5,23 @@
* 2.0.
*/
import type {
PluginSetupContract as ActionsPluginSetup,
PluginStartContract as ActionsPluginStart,
} from '@kbn/actions-plugin/server';
import type {
PluginSetupContract as AlertingPluginSetup,
PluginStartContract as AlertingPluginStart,
} from '@kbn/alerting-plugin/server';
import type {
DataViewsServerPluginSetup,
DataViewsServerPluginStart,
} from '@kbn/data-views-plugin/server';
import type {
PluginStartContract as FeaturesPluginStart,
PluginSetupContract as FeaturesPluginSetup,
} from '@kbn/features-plugin/server';
import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type {
ObservabilityAIAssistantServerSetup,
ObservabilityAIAssistantServerStart,
@ -13,6 +30,13 @@ import type {
RuleRegistryPluginSetupContract,
RuleRegistryPluginStartContract,
} from '@kbn/rule-registry-plugin/server';
import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/server';
import type {
TaskManagerSetupContract,
TaskManagerStartContract,
} from '@kbn/task-manager-plugin/server';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ObservabilityAIAssistantAppServerStart {}
@ -22,9 +46,27 @@ export interface ObservabilityAIAssistantAppServerSetup {}
export interface ObservabilityAIAssistantAppPluginStartDependencies {
observabilityAIAssistant: ObservabilityAIAssistantServerStart;
ruleRegistry: RuleRegistryPluginStartContract;
alerting: AlertingPluginStart;
licensing: LicensingPluginStart;
actions: ActionsPluginStart;
security: SecurityPluginStart;
features: FeaturesPluginStart;
taskManager: TaskManagerStartContract;
dataViews: DataViewsServerPluginStart;
cloud?: CloudStart;
serverless?: ServerlessPluginStart;
}
export interface ObservabilityAIAssistantAppPluginSetupDependencies {
observabilityAIAssistant: ObservabilityAIAssistantServerSetup;
ruleRegistry: RuleRegistryPluginSetupContract;
alerting: AlertingPluginSetup;
licensing: LicensingPluginSetup;
actions: ActionsPluginSetup;
security: SecurityPluginSetup;
features: FeaturesPluginSetup;
taskManager: TaskManagerSetupContract;
dataViews: DataViewsServerPluginSetup;
cloud?: CloudSetup;
serverless?: ServerlessPluginSetup;
}

View file

@ -55,7 +55,13 @@
"@kbn/ai-assistant-management-plugin",
"@kbn/deeplinks-observability",
"@kbn/management-settings-ids",
"@kbn/apm-utils"
"@kbn/apm-utils",
"@kbn/alerting-plugin",
"@kbn/stack-connectors-plugin",
"@kbn/features-plugin",
"@kbn/serverless",
"@kbn/task-manager-plugin",
"@kbn/cloud-plugin"
],
"exclude": ["target/**/*"]
}