From 90203d9d94b918716898ce197f5ba4d46ccc69ef Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Sat, 15 Jul 2023 08:12:13 +0200 Subject: [PATCH 01/23] Observability AI Assistant plugin --- package.json | 1 + tsconfig.base.json | 2 + .../observability_ai_assistant/README.md | 3 ++ .../observability_ai_assistant/jest.config.js | 14 +++++ .../observability_ai_assistant/kibana.jsonc | 17 +++++++ .../public/index.ts | 23 +++++++++ .../public/plugin.ts | 38 ++++++++++++++ .../public/types.ts | 15 ++++++ .../server/config.ts | 14 +++++ .../server/index.ts | 13 +++++ .../server/plugin.ts | 51 +++++++++++++++++++ .../server/types.ts | 12 +++++ .../observability_ai_assistant/tsconfig.json | 20 ++++++++ yarn.lock | 4 ++ 14 files changed, 227 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/README.md create mode 100644 x-pack/plugins/observability_ai_assistant/jest.config.js create mode 100644 x-pack/plugins/observability_ai_assistant/kibana.jsonc create mode 100644 x-pack/plugins/observability_ai_assistant/public/index.ts create mode 100644 x-pack/plugins/observability_ai_assistant/public/plugin.ts create mode 100644 x-pack/plugins/observability_ai_assistant/public/types.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/config.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/index.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/plugin.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/types.ts create mode 100644 x-pack/plugins/observability_ai_assistant/tsconfig.json diff --git a/package.json b/package.json index fe7881f13dcdb..c7fccfb14f4ae 100644 --- a/package.json +++ b/package.json @@ -526,6 +526,7 @@ "@kbn/newsfeed-test-plugin": "link:test/common/plugins/newsfeed", "@kbn/notifications-plugin": "link:x-pack/plugins/notifications", "@kbn/object-versioning": "link:packages/kbn-object-versioning", + "@kbn/observability-ai-assistant-plugin": "link:x-pack/plugins/observability_ai_assistant", "@kbn/observability-alert-details": "link:x-pack/packages/observability/alert_details", "@kbn/observability-fixtures-plugin": "link:x-pack/test/cases_api_integration/common/plugins/observability", "@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_onboarding", diff --git a/tsconfig.base.json b/tsconfig.base.json index d65e5ad3cb72d..0d416aa4ba817 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1014,6 +1014,8 @@ "@kbn/notifications-plugin/*": ["x-pack/plugins/notifications/*"], "@kbn/object-versioning": ["packages/kbn-object-versioning"], "@kbn/object-versioning/*": ["packages/kbn-object-versioning/*"], + "@kbn/observability-ai-assistant-plugin": ["x-pack/plugins/observability_ai_assistant"], + "@kbn/observability-ai-assistant-plugin/*": ["x-pack/plugins/observability_ai_assistant/*"], "@kbn/observability-alert-details": ["x-pack/packages/observability/alert_details"], "@kbn/observability-alert-details/*": ["x-pack/packages/observability/alert_details/*"], "@kbn/observability-fixtures-plugin": ["x-pack/test/cases_api_integration/common/plugins/observability"], diff --git a/x-pack/plugins/observability_ai_assistant/README.md b/x-pack/plugins/observability_ai_assistant/README.md new file mode 100644 index 0000000000000..1e2a19618825e --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/README.md @@ -0,0 +1,3 @@ +# Observability AI Assistant plugin + +This plugin provides the Observability AI Assistant service and UI components. diff --git a/x-pack/plugins/observability_ai_assistant/jest.config.js b/x-pack/plugins/observability_ai_assistant/jest.config.js new file mode 100644 index 0000000000000..e4a140341d07f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/jest.config.js @@ -0,0 +1,14 @@ +/* + * 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. + */ + +const path = require('path'); + +module.exports = { + preset: '@kbn/test', + rootDir: path.resolve(__dirname, '../../..'), + roots: ['/x-pack/plugins/observability_ai_assistant'], +}; diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc new file mode 100644 index 0000000000000..5079239c555fa --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -0,0 +1,17 @@ +{ + "type": "plugin", + "id": "@kbn/observability-ai-assistant-plugin", + "owner": "@elastic/apm-ui", + "plugin": { + "id": "observabilityAiAssistant", + "server": true, + "browser": true, + "configPath": [ + "xpack", + "observabilityAiAssistant" + ], + "requiredPlugins": [], + "optionalPlugins": [], + "extraPublicDirs": [] + } +} diff --git a/x-pack/plugins/observability_ai_assistant/public/index.ts b/x-pack/plugins/observability_ai_assistant/public/index.ts new file mode 100644 index 0000000000000..94587cdbb7ddd --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/index.ts @@ -0,0 +1,23 @@ +/* + * 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 type { PluginInitializer, PluginInitializerContext } from '@kbn/core/public'; +import { ObservabilityAIAssistantPlugin } from './plugin'; +import type { + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginStart, + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies, + ConfigSchema, +} from './types'; + +export const plugin: PluginInitializer< + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginStart, + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies +> = (pluginInitializerContext: PluginInitializerContext) => + new ObservabilityAIAssistantPlugin(pluginInitializerContext); diff --git a/x-pack/plugins/observability_ai_assistant/public/plugin.ts b/x-pack/plugins/observability_ai_assistant/public/plugin.ts new file mode 100644 index 0000000000000..6cafbfc55555b --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.ts @@ -0,0 +1,38 @@ +/* + * 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 type { Plugin, PluginInitializerContext } from '@kbn/core/public'; +import type { Logger } from '@kbn/logging'; +import type { + ConfigSchema, + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStart, + ObservabilityAIAssistantPluginStartDependencies, +} from './types'; + +export class ObservabilityAIAssistantPlugin + implements + Plugin< + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginStart, + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies + > +{ + logger: Logger; + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + setup() { + return {}; + } + + start() { + return {}; + } +} diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts new file mode 100644 index 0000000000000..1b091c9bffa01 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface*/ + +export interface ObservabilityAIAssistantPluginStart {} +export interface ObservabilityAIAssistantPluginSetup {} +export interface ObservabilityAIAssistantPluginSetupDependencies {} +export interface ObservabilityAIAssistantPluginStartDependencies {} + +export interface ConfigSchema {} diff --git a/x-pack/plugins/observability_ai_assistant/server/config.ts b/x-pack/plugins/observability_ai_assistant/server/config.ts new file mode 100644 index 0000000000000..61c67ad3e19a9 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/config.ts @@ -0,0 +1,14 @@ +/* + * 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'; + +export const config = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ObservabilityAIAssistantConfig = TypeOf; diff --git a/x-pack/plugins/observability_ai_assistant/server/index.ts b/x-pack/plugins/observability_ai_assistant/server/index.ts new file mode 100644 index 0000000000000..b44fd9edace38 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/index.ts @@ -0,0 +1,13 @@ +/* + * 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 type { PluginInitializerContext } from '@kbn/core/server'; +import type { ObservabilityAIAssistantConfig } from './config'; +import { ObservabilityAIAssistantPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => + new ObservabilityAIAssistantPlugin(ctx); diff --git a/x-pack/plugins/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_ai_assistant/server/plugin.ts new file mode 100644 index 0000000000000..db10c627beed4 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/plugin.ts @@ -0,0 +1,51 @@ +/* + * 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 type { + CoreSetup, + CoreStart, + Logger, + Plugin, + PluginInitializerContext, +} from '@kbn/core/server'; +import type { ObservabilityAIAssistantConfig } from './config'; +import { + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginStart, + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies, +} from './types'; + +export class ObservabilityAIAssistantPlugin + implements + Plugin< + ObservabilityAIAssistantPluginSetup, + ObservabilityAIAssistantPluginStart, + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies + > +{ + logger: Logger; + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + } + public start( + core: CoreStart, + plugins: ObservabilityAIAssistantPluginSetupDependencies + ): ObservabilityAIAssistantPluginStart { + return {}; + } + public setup( + core: CoreSetup< + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStart + >, + plugins: ObservabilityAIAssistantPluginSetupDependencies + ): ObservabilityAIAssistantPluginSetup { + return {}; + } +} diff --git a/x-pack/plugins/observability_ai_assistant/server/types.ts b/x-pack/plugins/observability_ai_assistant/server/types.ts new file mode 100644 index 0000000000000..b4ad16729c5bd --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/types.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/no-empty-interface*/ +export interface ObservabilityAIAssistantPluginStart {} +export interface ObservabilityAIAssistantPluginSetup {} +export interface ObservabilityAIAssistantPluginSetupDependencies {} +export interface ObservabilityAIAssistantPluginStartDependencies {} diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json new file mode 100644 index 0000000000000..9491b336236bd --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + }, + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "typings/**/*", + "public/**/*.json", + "server/**/*" + ], + "kbn_references": [ + "@kbn/core" + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/yarn.lock b/yarn.lock index 2694b8f50b597..4005cd9faceda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4762,6 +4762,10 @@ version "0.0.0" uid "" +"@kbn/observability-ai-assistant-plugin@link:x-pack/plugins/observability_ai_assistant": + version "0.0.0" + uid "" + "@kbn/observability-alert-details@link:x-pack/packages/observability/alert_details": version "0.0.0" uid "" From 798b1c3b0f111206ee314c074c05d8e77c1aff3a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 18 Jul 2023 11:12:57 +0200 Subject: [PATCH 02/23] Observability AI Assistant Service + Client --- package.json | 5 +- packages/kbn-server-route-repository/index.ts | 2 + .../src/register_routes.ts | 141 ++++++++++++ .../kbn-server-route-repository/tsconfig.json | 6 +- .../common/types.ts | 56 +++++ .../observability_ai_assistant/kibana.jsonc | 9 +- .../public/types.ts | 12 +- .../server/plugin.ts | 35 ++- .../server/routes/chat/route.ts | 42 ++++ .../server/routes/conversations/route.ts | 140 +++++++++++ ...observability_ai_assistant_server_route.ts | 16 ++ ...rvability_ai_assistant_route_repository.ts | 20 ++ .../server/routes/register_routes.ts | 31 +++ .../server/routes/runtime_types.ts | 92 ++++++++ .../server/routes/types.ts | 36 +++ .../server/service/client/index.ts | 217 ++++++++++++++++++ .../conversation_component_template.ts | 91 ++++++++ .../server/service/index.ts | 191 +++++++++++++++ .../server/service/types.ts | 48 ++++ .../server/types.ts | 10 +- .../observability_ai_assistant/tsconfig.json | 9 +- yarn.lock | 13 +- 22 files changed, 1205 insertions(+), 17 deletions(-) create mode 100644 packages/kbn-server-route-repository/src/register_routes.ts create mode 100644 x-pack/plugins/observability_ai_assistant/common/types.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/create_observability_ai_assistant_server_route.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/register_routes.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/routes/types.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/service/client/index.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/service/conversation_component_template.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/service/index.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/service/types.ts diff --git a/package.json b/package.json index c7fccfb14f4ae..afc2bdb905e05 100644 --- a/package.json +++ b/package.json @@ -768,6 +768,7 @@ "@opentelemetry/sdk-metrics-base": "^0.31.0", "@opentelemetry/semantic-conventions": "^1.4.0", "@reduxjs/toolkit": "1.7.2", + "@sindresorhus/fnv1a": "^3.0.0", "@slack/webhook": "^5.0.4", "@tanstack/react-query": "^4.29.12", "@tanstack/react-query-devtools": "^4.29.12", @@ -901,7 +902,7 @@ "normalize-path": "^3.0.0", "object-hash": "^1.3.1", "object-path-immutable": "^3.1.1", - "openai": "^3.2.1", + "openai": "^3.3.0", "openpgp": "5.3.0", "opn": "^5.5.0", "ora": "^4.0.4", @@ -1545,4 +1546,4 @@ "xmlbuilder": "13.0.2", "yargs": "^15.4.1" } -} \ No newline at end of file +} diff --git a/packages/kbn-server-route-repository/index.ts b/packages/kbn-server-route-repository/index.ts index ebf158b89f76e..95007711ca0aa 100644 --- a/packages/kbn-server-route-repository/index.ts +++ b/packages/kbn-server-route-repository/index.ts @@ -11,6 +11,8 @@ export { formatRequest } from './src/format_request'; export { parseEndpoint } from './src/parse_endpoint'; export { decodeRequestParams } from './src/decode_request_params'; export { routeValidationObject } from './src/route_validation_object'; +export { registerRoutes } from './src/register_routes'; + export type { RouteRepositoryClient, ReturnOf, diff --git a/packages/kbn-server-route-repository/src/register_routes.ts b/packages/kbn-server-route-repository/src/register_routes.ts new file mode 100644 index 0000000000000..6698f40b2d5c6 --- /dev/null +++ b/packages/kbn-server-route-repository/src/register_routes.ts @@ -0,0 +1,141 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { errors } from '@elastic/elasticsearch'; +import { isBoom } from '@hapi/boom'; +import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; +import type { KibanaRequest, KibanaResponseFactory } from '@kbn/core-http-server'; +import type { CoreSetup } from '@kbn/core-lifecycle-server'; +import type { Logger } from '@kbn/logging'; +import * as t from 'io-ts'; +import { merge, pick } from 'lodash'; +import { decodeRequestParams } from './decode_request_params'; +import { parseEndpoint } from './parse_endpoint'; +import { routeValidationObject } from './route_validation_object'; +import type { ServerRoute, ServerRouteCreateOptions } from './typings'; + +const CLIENT_CLOSED_REQUEST = { + statusCode: 499, + body: { + message: 'Client closed request', + }, +}; + +export function registerRoutes({ + core, + repository, + logger, + dependencies, +}: { + core: CoreSetup; + repository: Record>; + logger: Logger; + dependencies: Record; +}) { + const routes = Object.values(repository); + + const router = core.http.createRouter(); + + routes.forEach((route) => { + const { params, endpoint, options, handler } = route; + + const { method, pathname, version } = parseEndpoint(endpoint); + + const wrappedHandler = async ( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) => { + try { + const runtimeType = params || t.strict({}); + + const validatedParams = decodeRequestParams( + pick(request, 'params', 'body', 'query'), + runtimeType + ); + + const { aborted, data } = await Promise.race([ + handler({ + request, + context, + params: validatedParams, + ...dependencies, + }).then((value) => { + return { + aborted: false, + data: value, + }; + }), + request.events.aborted$.toPromise().then(() => { + return { + aborted: true, + data: undefined, + }; + }), + ]); + + if (aborted) { + return response.custom(CLIENT_CLOSED_REQUEST); + } + + const body = data || {}; + + return response.ok({ body }); + } catch (error) { + logger.error(error); + + const opts = { + statusCode: 500, + body: { + message: error.message, + attributes: { + data: {}, + }, + }, + }; + + if (error instanceof errors.RequestAbortedError) { + return response.custom(merge(opts, CLIENT_CLOSED_REQUEST)); + } + + if (isBoom(error)) { + opts.statusCode = error.output.statusCode; + opts.body.attributes.data = error?.data; + } + + return response.custom(opts); + } + }; + + logger.debug(`Registering endpoint ${endpoint}`); + + if (!version) { + router[method]( + { + path: pathname, + options, + validate: routeValidationObject, + }, + wrappedHandler + ); + } else { + router.versioned[method]({ + path: pathname, + access: pathname.startsWith('/internal/') ? 'internal' : 'public', + options, + }).addVersion( + { + version, + validate: { + request: routeValidationObject, + }, + }, + wrappedHandler + ); + } + }); +} diff --git a/packages/kbn-server-route-repository/tsconfig.json b/packages/kbn-server-route-repository/tsconfig.json index 68e576e00b06e..a0e9cc288d7b0 100644 --- a/packages/kbn-server-route-repository/tsconfig.json +++ b/packages/kbn-server-route-repository/tsconfig.json @@ -13,7 +13,11 @@ ], "kbn_references": [ "@kbn/config-schema", - "@kbn/io-ts-utils" + "@kbn/io-ts-utils", + "@kbn/core-http-request-handler-context-server", + "@kbn/core-http-server", + "@kbn/core-lifecycle-server", + "@kbn/logging" ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts new file mode 100644 index 0000000000000..5cdabdceb5a2c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -0,0 +1,56 @@ +/* + * 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 { Serializable } from '@kbn/utility-types'; + +export enum MessageRole { + System = 'system', + Assistant = 'assistant', + User = 'user', + Function = 'function', + Event = 'event', + Elastic = 'elastic', +} + +export interface Message { + '@timestamp': string; + message: { + content?: string; + name?: string; + role: MessageRole; + function_call?: { + name: string; + args?: Serializable; + trigger: MessageRole.Assistant | MessageRole.User | MessageRole.Elastic; + }; + data?: Serializable; + }; +} + +export interface Conversation { + '@timestamp': string; + user: { + id?: string; + name: string; + }; + conversation: { + id: string; + title: string; + last_updated: string; + }; + messages: Message[]; + labels: Record; + numeric_labels: Record; + namespace: string; +} + +export type ConversationRequestBase = Omit & { + conversation: { title: string }; +}; + +export type ConversationCreateRequest = ConversationRequestBase; +export type ConversationUpdateRequest = ConversationRequestBase & { conversation: { id: string } }; diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index 5079239c555fa..c3a1dcd41a270 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -3,14 +3,17 @@ "id": "@kbn/observability-ai-assistant-plugin", "owner": "@elastic/apm-ui", "plugin": { - "id": "observabilityAiAssistant", + "id": "observabilityAIAssistant", "server": true, "browser": true, "configPath": [ "xpack", - "observabilityAiAssistant" + "observabilityAIAssistant" + ], + "requiredPlugins": [ + "triggersActionsUi", + "actions" ], - "requiredPlugins": [], "optionalPlugins": [], "extraPublicDirs": [] } diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 1b091c9bffa01..68f18af530f8f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -4,12 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { + TriggersAndActionsUIPublicPluginSetup, + TriggersAndActionsUIPublicPluginStart, +} from '@kbn/triggers-actions-ui-plugin/public'; /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ObservabilityAIAssistantPluginStart {} export interface ObservabilityAIAssistantPluginSetup {} -export interface ObservabilityAIAssistantPluginSetupDependencies {} -export interface ObservabilityAIAssistantPluginStartDependencies {} +export interface ObservabilityAIAssistantPluginSetupDependencies { + triggersActions: TriggersAndActionsUIPublicPluginSetup; +} +export interface ObservabilityAIAssistantPluginStartDependencies { + triggersActions: TriggersAndActionsUIPublicPluginStart; +} export interface ConfigSchema {} diff --git a/x-pack/plugins/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_ai_assistant/server/plugin.ts index db10c627beed4..23efe49ab24a7 100644 --- a/x-pack/plugins/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_ai_assistant/server/plugin.ts @@ -12,7 +12,11 @@ import type { Plugin, PluginInitializerContext, } from '@kbn/core/server'; +import { mapValues } from 'lodash'; import type { ObservabilityAIAssistantConfig } from './config'; +import { registerServerRoutes } from './routes/register_routes'; +import { ObservabilityAIAssistantRouteHandlerResources } from './routes/types'; +import { ObservabilityAIAssistantService } from './service'; import { ObservabilityAIAssistantPluginSetup, ObservabilityAIAssistantPluginStart, @@ -35,17 +39,44 @@ export class ObservabilityAIAssistantPlugin } public start( core: CoreStart, - plugins: ObservabilityAIAssistantPluginSetupDependencies + plugins: ObservabilityAIAssistantPluginStartDependencies ): ObservabilityAIAssistantPluginStart { return {}; } public setup( core: CoreSetup< - ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies, ObservabilityAIAssistantPluginStart >, plugins: ObservabilityAIAssistantPluginSetupDependencies ): ObservabilityAIAssistantPluginSetup { + 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 service = new ObservabilityAIAssistantService({ + logger: this.logger.get('service'), + core, + }); + + registerServerRoutes({ + core, + logger: this.logger, + dependencies: { + plugins: routeHandlerPlugins, + service, + }, + }); + return {}; } } diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts new file mode 100644 index 0000000000000..fc806cc332ebf --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -0,0 +1,42 @@ +/* + * 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 * as t from 'io-ts'; +import type { IncomingMessage } from 'http'; +import { notImplemented } from '@hapi/boom'; +import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; +import { conversationRt } from '../runtime_types'; + +const chatRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/chat', + options: { + tags: ['access:ai_assistant'], + }, + params: t.type({ + body: t.type({ + conversation: conversationRt, + connectorId: t.string, + }), + }), + handler: async (resources): Promise => { + const { request, params, service } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + return client.chat({ + messages: params.body.conversation.messages, + connectorId: params.body.connectorId, + }); + }, +}); + +export const chatRoutes = { + ...chatRoute, +}; diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts new file mode 100644 index 0000000000000..72fe03f27e845 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts @@ -0,0 +1,140 @@ +/* + * 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 { notImplemented } from '@hapi/boom'; +import * as t from 'io-ts'; +import { merge } from 'lodash'; +import { Conversation } from '../../../common/types'; +import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; +import { conversationCreateRt, conversationUpdateRt } from '../runtime_types'; + +const getConversationRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}', + params: t.type({ + path: t.type({ + conversationId: t.string, + }), + }), + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + return client.get(params.path.conversationId); + }, +}); + +const findConversationsRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/conversations', + params: t.partial({ + body: t.partial({ + query: t.string, + }), + }), + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise<{ conversations: Conversation[] }> => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + return client.find({ query: params?.body?.query }); + }, +}); + +const createConversationRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'PUT /internal/observability_ai_assistant/conversation', + params: t.type({ + body: t.type({ + conversation: conversationCreateRt, + }), + }), + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + return client.create(params.body.conversation); + }, +}); + +const updateConversationRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/conversation/{conversationId}', + params: t.type({ + path: t.type({ + conversationId: t.string, + }), + body: t.type({ + conversation: conversationUpdateRt, + }), + }), + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + return client.create( + merge({}, params.body.conversation, { conversation: { id: params.path.conversationId } }) + ); + }, +}); + +const deleteConversationRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', + params: t.type({ + path: t.type({ + conversationId: t.string, + }), + }), + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + return client.delete(params.path.conversationId); + }, +}); + +export const conversationRoutes = { + ...getConversationRoute, + ...findConversationsRoute, + ...createConversationRoute, + ...updateConversationRoute, + ...deleteConversationRoute, +}; diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/create_observability_ai_assistant_server_route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/create_observability_ai_assistant_server_route.ts new file mode 100644 index 0000000000000..40120da94065c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/create_observability_ai_assistant_server_route.ts @@ -0,0 +1,16 @@ +/* + * 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 { createServerRouteFactory } from '@kbn/server-route-repository'; +import type { + ObservabilityAIAssistantRouteCreateOptions, + ObservabilityAIAssistantRouteHandlerResources, +} from './types'; + +export const createObservabilityAIAssistantServerRoute = createServerRouteFactory< + ObservabilityAIAssistantRouteHandlerResources, + ObservabilityAIAssistantRouteCreateOptions +>(); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts b/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts new file mode 100644 index 0000000000000..5fa24a13555ca --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/get_global_observability_ai_assistant_route_repository.ts @@ -0,0 +1,20 @@ +/* + * 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 { chatRoutes } from './chat/route'; +import { conversationRoutes } from './conversations/route'; + +export function getGlobalObservabilityAIAssistantServerRouteRepository() { + return { + ...chatRoutes, + ...conversationRoutes, + }; +} + +export type ObservabilityAIAssistantServerRouteRepository = ReturnType< + typeof getGlobalObservabilityAIAssistantServerRouteRepository +>; diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/register_routes.ts b/x-pack/plugins/observability_ai_assistant/server/routes/register_routes.ts new file mode 100644 index 0000000000000..cd1bccdda6734 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/register_routes.ts @@ -0,0 +1,31 @@ +/* + * 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 type { CoreSetup } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { registerRoutes } from '@kbn/server-route-repository'; +import { getGlobalObservabilityAIAssistantServerRouteRepository } from './get_global_observability_ai_assistant_route_repository'; +import type { ObservabilityAIAssistantRouteHandlerResources } from './types'; + +export function registerServerRoutes({ + core, + logger, + dependencies, +}: { + core: CoreSetup; + logger: Logger; + dependencies: Omit< + ObservabilityAIAssistantRouteHandlerResources, + 'request' | 'context' | 'logger' | 'params' + >; +}) { + registerRoutes({ + core, + logger, + repository: getGlobalObservabilityAIAssistantServerRouteRepository(), + dependencies, + }); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts new file mode 100644 index 0000000000000..49dcb8ec1a930 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts @@ -0,0 +1,92 @@ +/* + * 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 * as t from 'io-ts'; +import { + Conversation, + ConversationCreateRequest, + ConversationRequestBase, + ConversationUpdateRequest, + Message, + MessageRole, +} from '../../common/types'; + +const serializeableRt = t.any; + +export const messageRt: t.Type = t.type({ + '@timestamp': t.string, + message: t.intersection([ + t.type({ + role: t.union([ + t.literal(MessageRole.System), + t.literal(MessageRole.Assistant), + t.literal(MessageRole.Event), + t.literal(MessageRole.Function), + t.literal(MessageRole.User), + t.literal(MessageRole.Elastic), + ]), + }), + t.partial({ + content: t.string, + name: t.string, + function_call: t.intersection([ + t.type({ + name: t.string, + trigger: t.union([ + t.literal(MessageRole.Assistant), + t.literal(MessageRole.User), + t.literal(MessageRole.Elastic), + ]), + }), + t.partial({ + args: serializeableRt, + data: serializeableRt, + }), + ]), + }), + ]), +}); + +export const baseConversationRt: t.Type = t.type({ + '@timestamp': t.string, + conversation: t.type({ + title: t.string, + }), + messages: t.array(messageRt), + labels: t.record(t.string, t.string), + numeric_labels: t.record(t.string, t.number), +}); + +export const conversationCreateRt: t.Type = t.intersection([ + baseConversationRt, + t.type({ + conversation: t.type({ + title: t.string, + }), + }), +]); + +export const conversationUpdateRt: t.Type = t.intersection([ + baseConversationRt, + t.type({ + conversation: t.type({ + id: t.string, + title: t.string, + }), + }), +]); + +export const conversationRt: t.Type = t.intersection([ + baseConversationRt, + t.type({ + user: t.intersection([t.type({ name: t.string }), t.partial({ id: t.string })]), + namespace: t.string, + conversation: t.type({ + id: t.string, + last_updated: t.string, + }), + }), +]); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/types.ts new file mode 100644 index 0000000000000..bf7250eb80d30 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/routes/types.ts @@ -0,0 +1,36 @@ +/* + * 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 type { Logger } from '@kbn/logging'; +import type { KibanaRequest, RequestHandlerContext } from '@kbn/core/server'; +import type { + ObservabilityAIAssistantPluginSetupDependencies, + ObservabilityAIAssistantPluginStartDependencies, +} from '../types'; +import type { IObservabilityAIAssistantService } from '../service/types'; + +export interface ObservabilityAIAssistantRouteHandlerResources { + request: KibanaRequest; + context: RequestHandlerContext; + logger: Logger; + service: IObservabilityAIAssistantService; + plugins: { + [key in keyof ObservabilityAIAssistantPluginSetupDependencies]: { + setup: Required[key]; + }; + } & { + [key in keyof ObservabilityAIAssistantPluginStartDependencies]: { + start: () => Promise[key]>; + }; + }; +} + +export interface ObservabilityAIAssistantRouteCreateOptions { + options: { + tags: Array<'access:ai_assistant'>; + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts new file mode 100644 index 0000000000000..7dacfc704d108 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -0,0 +1,217 @@ +/* + * 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 { v4 } from 'uuid'; + +import type { ChatCompletionRequestMessage, CreateChatCompletionRequest } from 'openai'; +import type { IncomingMessage } from 'http'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import type { ActionsClient } from '@kbn/actions-plugin/server/actions_client'; +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { internal, notFound } from '@hapi/boom'; +import { compact, merge, omit } from 'lodash'; +import { SearchHit } from '@elastic/elasticsearch/lib/api/types'; +import { + type Conversation, + type ConversationCreateRequest, + type ConversationUpdateRequest, + type Message, + MessageRole, +} from '../../../common/types'; +import type { + IObservabilityAIAssistantClient, + ObservabilityAIAssistantResourceNames, +} from '../types'; + +export class ObservabilityAIAssistantClient implements IObservabilityAIAssistantClient { + constructor( + private readonly dependencies: { + actionsClient: PublicMethodsOf; + namespace: string; + esClient: ElasticsearchClient; + resources: ObservabilityAIAssistantResourceNames; + logger: Logger; + user: { + id?: string; + name: string; + }; + } + ) {} + + private getAccessQuery() { + return [ + { + bool: { + filter: [ + { + term: { + 'user.name': this.dependencies.user.name, + }, + }, + { + term: { + namespace: this.dependencies.namespace, + }, + }, + ], + }, + }, + ]; + } + + private getConversationWithMetaFields = async ( + conversationId: string + ): Promise | undefined> => { + const response = await this.dependencies.esClient.search({ + index: this.dependencies.resources.aliases.conversations, + query: { + bool: { + filter: [...this.getAccessQuery(), { term: { 'conversation.id': conversationId } }], + }, + }, + size: 1, + terminate_after: 1, + }); + + return response.hits.hits[0]; + }; + + private getConversationUpdateValues = (now: string) => { + return { + conversation: { + last_updated: now, + }, + user: this.dependencies.user, + namespace: this.dependencies.namespace, + }; + }; + + get = async (conversationId: string): Promise => { + return (await this.getConversationWithMetaFields(conversationId))?._source; + }; + + delete = async (conversationId: string): Promise => { + const conversation = await this.getConversationWithMetaFields(conversationId); + + if (!conversation) { + throw notFound(); + } + + await this.dependencies.esClient.delete({ + id: conversation._id, + index: conversation._index, + }); + }; + + chat = async ({ + messages, + connectorId, + }: { + messages: Message[]; + connectorId: string; + }): Promise => { + const messagesForOpenAI: ChatCompletionRequestMessage[] = compact( + messages.map((message) => { + if (message.message.role === MessageRole.Event) { + return undefined; + } + const role = + message.message.role === MessageRole.Elastic ? MessageRole.User : message.message.role; + + return { + role, + content: message.message.content, + function_call: omit(message.message.function_call, 'trigger'), + name: message.message.name, + }; + }) + ); + + const request: CreateChatCompletionRequest = { + model: 'gpt-4', + messages: messagesForOpenAI, + stream: true, + }; + + const executeResult = await this.dependencies.actionsClient.execute({ + actionId: connectorId, + params: { + subAction: 'stream', + subActionParams: JSON.stringify(request), + }, + }); + + if (executeResult.status === 'error') { + throw internal(`${executeResult?.message} - ${executeResult?.serviceMessage}`); + } + + return executeResult.data as IncomingMessage; + }; + + find = async (options?: { query?: string }): Promise<{ conversations: Conversation[] }> => { + const response = await this.dependencies.esClient.search({ + index: this.dependencies.resources.aliases.conversations, + allow_no_indices: true, + query: { + bool: { + filter: [...this.getAccessQuery()], + }, + }, + sort: { + '@timestamp': 'desc', + }, + size: 100, + }); + + return { + conversations: response.hits.hits.map((hit) => hit._source!), + }; + }; + + update = async (conversation: ConversationUpdateRequest): Promise => { + const document = await this.getConversationWithMetaFields(conversation.conversation.id); + + if (!document) { + throw notFound(); + } + + const updatedConversation: Conversation = merge( + {}, + conversation, + this.getConversationUpdateValues(new Date().toISOString()) + ); + + await this.dependencies.esClient.update({ + id: document._id, + index: document._index, + doc: updatedConversation, + }); + + return updatedConversation; + }; + + create = async (conversation: ConversationCreateRequest): Promise => { + const now = new Date().toISOString(); + + const createdConversation: Conversation = merge( + {}, + conversation, + { + '@timestamp': now, + conversation: { id: v4() }, + }, + this.getConversationUpdateValues(now) + ); + + await this.dependencies.esClient.index({ + index: this.dependencies.resources.aliases.conversations, + document: createdConversation, + }); + + return createdConversation; + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/conversation_component_template.ts b/x-pack/plugins/observability_ai_assistant/server/service/conversation_component_template.ts new file mode 100644 index 0000000000000..b758d702d2c43 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/conversation_component_template.ts @@ -0,0 +1,91 @@ +/* + * 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 { ClusterComponentTemplate } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +const keyword = { + type: 'keyword' as const, + ignore_above: 1024, +}; + +const text = { + type: 'text' as const, +}; + +const date = { + type: 'date' as const, +}; + +const dynamic = { + type: 'object' as const, + dynamic: true, +}; + +export const conversationComponentTemplate: ClusterComponentTemplate['component_template']['template'] = + { + mappings: { + dynamic_templates: [ + { + numeric_labels: { + path_match: 'numeric_labels.*', + mapping: { + scaling_factor: 1000000, + type: 'scaled_float', + }, + }, + }, + ], + dynamic: false, + properties: { + '@timestamp': date, + labels: dynamic, + numeric_labels: dynamic, + user: { + properties: { + id: keyword, + name: keyword, + }, + }, + conversation: { + properties: { + id: keyword, + title: text, + last_updated: date, + }, + }, + namespace: keyword, + messages: { + type: 'object', + properties: { + '@timestamp': date, + message: { + type: 'object', + properties: { + content: text, + role: keyword, + data: { + type: 'object', + enabled: false, + }, + function_call: { + type: 'object', + properties: { + name: keyword, + args: { + type: 'object', + enabled: false, + }, + trigger: keyword, + }, + }, + }, + }, + }, + }, + }, + }, + }; diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts new file mode 100644 index 0000000000000..eeac8253a0626 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -0,0 +1,191 @@ +/* + * 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 stringify from 'json-stable-stringify'; +import type { CoreSetup, CoreStart, KibanaRequest, Logger } from '@kbn/core/server'; +import { once } from 'lodash'; +import type { SecurityPluginStart } from '@kbn/security-plugin/server'; +import * as Boom from '@hapi/boom'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server/plugin'; +import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; +import type { ObservabilityAIAssistantResourceNames } from './types'; +import { conversationComponentTemplate } from './conversation_component_template'; +import type { IObservabilityAIAssistantClient, IObservabilityAIAssistantService } from './types'; +import { ObservabilityAIAssistantClient } from './client'; + +function getResourceName(resource: string) { + return `.kibana-observability-ai-assistant-${resource}`; +} + +export class ObservabilityAIAssistantService implements IObservabilityAIAssistantService { + private readonly core: CoreSetup; + private readonly logger: Logger; + + private readonly resourceNames: ObservabilityAIAssistantResourceNames = { + componentTemplate: { + conversations: getResourceName('component-template-conversations'), + }, + aliases: { + conversations: getResourceName('conversations'), + }, + indexPatterns: { + conversations: getResourceName('conversations*'), + }, + indexTemplate: { + conversations: getResourceName('index-template-conversations'), + }, + ilmPolicy: { + conversations: getResourceName('ilm-policy-conversations'), + }, + }; + + constructor({ logger, core }: { logger: Logger; core: CoreSetup }) { + this.core = core; + this.logger = logger; + + this.init(); + } + + init = once(async () => { + try { + const [coreStart] = await this.core.getStartServices(); + + const esClient = coreStart.elasticsearch.client.asInternalUser; + + const fnv1a = await import('@sindresorhus/fnv1a'); + + const versionHash = fnv1a.default(stringify(conversationComponentTemplate), { size: 64 }); + + await esClient.cluster.putComponentTemplate({ + create: false, + name: this.resourceNames.componentTemplate.conversations, + template: { + ...conversationComponentTemplate, + mappings: { + _meta: { + version: versionHash, + }, + ...conversationComponentTemplate.mappings, + }, + }, + }); + + await esClient.ilm.putLifecycle({ + name: this.resourceNames.ilmPolicy.conversations, + policy: { + phases: { + hot: { + min_age: '0s', + actions: { + rollover: { + max_age: '90d', + max_primary_shard_size: '50gb', + }, + }, + }, + }, + }, + }); + + await esClient.indices.putIndexTemplate({ + name: this.resourceNames.indexTemplate.conversations, + composed_of: [this.resourceNames.componentTemplate.conversations], + create: false, + index_patterns: [this.resourceNames.indexPatterns.conversations], + template: { + settings: { + number_of_shards: 1, + auto_expand_replicas: '0-1', + refresh_interval: '1s', + }, + }, + }); + + const aliasName = this.resourceNames.aliases.conversations; + + const aliasExists = await esClient.indices.existsAlias({ + name: aliasName, + }); + + if (!aliasExists) { + const firstIndexName = `${this.resourceNames.aliases.conversations}-000001`; + await esClient.indices.create({ + index: firstIndexName, + aliases: { + [aliasName]: { + is_write_index: true, + }, + }, + }); + } + + const indicesForAlias = await esClient.indices.get({ + index: aliasName, + }); + + const writeIndexName = Object.keys(indicesForAlias).find((indexName) => { + if (indicesForAlias[indexName]!.aliases?.[aliasName].is_write_index) { + return true; + } + return false; + }); + + if (!writeIndexName) { + throw new Error(`Expected write index for ${aliasName}, but none was found`); + } + + const writeIndex = indicesForAlias[writeIndexName]; + + if (writeIndex.mappings?._meta?.version !== versionHash) { + await esClient.indices.rollover({ + alias: aliasName, + conditions: { + min_docs: 0, + }, + }); + } + } catch (error) { + this.logger.error(`Failed to initialize CoPilotService: ${error.message}`); + this.logger.debug(error); + } + }); + + async getClient({ + request, + }: { + request: KibanaRequest; + }): Promise { + const [_, [coreStart, { security, actions }]] = await Promise.all([ + this.init(), + this.core.getStartServices() as Promise< + [CoreStart, { security: SecurityPluginStart; actions: ActionsPluginStart }, unknown] + >, + ]); + + const user = security.authc.getCurrentUser(request); + + if (!user) { + throw Boom.forbidden(`User not found for current request`); + } + + const basePath = coreStart.http.basePath.get(request); + + const { spaceId } = getSpaceIdFromPath(basePath, coreStart.http.basePath.serverBasePath); + + return new ObservabilityAIAssistantClient({ + actionsClient: await actions.getActionsClientWithRequest(request), + namespace: spaceId, + esClient: coreStart.elasticsearch.client.asInternalUser, + resources: this.resourceNames, + logger: this.logger, + user: { + id: user.profile_uid, + name: user.username, + }, + }); + } +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_ai_assistant/server/service/types.ts new file mode 100644 index 0000000000000..d8f0c6b2b5f09 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/types.ts @@ -0,0 +1,48 @@ +/* + * 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 { IncomingMessage } from 'http'; +import { KibanaRequest } from '@kbn/core/server'; +import { + Conversation, + ConversationCreateRequest, + ConversationUpdateRequest, + Message, +} from '../../common/types'; + +export interface IObservabilityAIAssistantClient { + chat: (options: { messages: Message[]; connectorId: string }) => Promise; + get: (conversationId: string) => void; + find: (options?: { query?: string }) => Promise<{ conversations: Conversation[] }>; + create: (conversation: ConversationCreateRequest) => Promise; + update: (conversation: ConversationUpdateRequest) => Promise; + delete: (conversationId: string) => Promise; +} + +export interface IObservabilityAIAssistantService { + getClient: (options: { + request: KibanaRequest; + }) => Promise; +} + +export interface ObservabilityAIAssistantResourceNames { + componentTemplate: { + conversations: string; + }; + indexTemplate: { + conversations: string; + }; + ilmPolicy: { + conversations: string; + }; + aliases: { + conversations: string; + }; + indexPatterns: { + conversations: string; + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/server/types.ts b/x-pack/plugins/observability_ai_assistant/server/types.ts index b4ad16729c5bd..c1753b1f9497f 100644 --- a/x-pack/plugins/observability_ai_assistant/server/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/types.ts @@ -5,8 +5,14 @@ * 2.0. */ +import { PluginSetupContract, PluginStartContract } from '@kbn/actions-plugin/server'; + /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ObservabilityAIAssistantPluginStart {} export interface ObservabilityAIAssistantPluginSetup {} -export interface ObservabilityAIAssistantPluginSetupDependencies {} -export interface ObservabilityAIAssistantPluginStartDependencies {} +export interface ObservabilityAIAssistantPluginSetupDependencies { + actions: PluginSetupContract; +} +export interface ObservabilityAIAssistantPluginStartDependencies { + actions: PluginStartContract; +} diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index 9491b336236bd..f723c0bc7a9d0 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -12,7 +12,14 @@ "server/**/*" ], "kbn_references": [ - "@kbn/core" + "@kbn/core", + "@kbn/actions-plugin", + "@kbn/utility-types", + "@kbn/server-route-repository", + "@kbn/logging", + "@kbn/triggers-actions-ui-plugin", + "@kbn/config-schema", + "@kbn/security-plugin" ], "exclude": [ "target/**/*", diff --git a/yarn.lock b/yarn.lock index 4005cd9faceda..6182d43121413 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6817,6 +6817,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sindresorhus/fnv1a@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/fnv1a/-/fnv1a-3.0.0.tgz#e8ce2e7c7738ec8c354867d38e3bfcde622b87ca" + integrity sha512-M6pmbdZqAryzjZ4ELAzrdCMoMZk5lH/fshKrapfSeXdf2W+GDqZvPmfXaNTZp43//FVbSwkTPwpEMnehSyskkQ== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -22392,10 +22397,10 @@ open@^8.0.9, open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -openai@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/openai/-/openai-3.2.1.tgz#1fa35bdf979cbde8453b43f2dd3a7d401ee40866" - integrity sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A== +openai@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-3.3.0.tgz#a6408016ad0945738e1febf43f2fccca83a3f532" + integrity sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ== dependencies: axios "^0.26.0" form-data "^4.0.0" From 5f96a6c5137560f228ce69b27780421fc8177e09 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 19 Jul 2023 10:16:14 +0200 Subject: [PATCH 03/23] Add Storybook setup --- src/dev/storybook/aliases.ts | 1 + .../.storybook/jest_setup.js | 11 +++++++++++ .../observability_ai_assistant/.storybook/main.js | 8 ++++++++ .../observability_ai_assistant/.storybook/preview.js | 10 ++++++++++ 4 files changed, 30 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/.storybook/jest_setup.js create mode 100644 x-pack/plugins/observability_ai_assistant/.storybook/main.js create mode 100644 x-pack/plugins/observability_ai_assistant/.storybook/preview.js diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 7dea24c44ee03..fa55036bf3d03 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -48,6 +48,7 @@ export const storybookAliases = { lists: 'x-pack/plugins/lists/.storybook', management: 'packages/kbn-management/storybook/config', observability: 'x-pack/plugins/observability/.storybook', + observability_ai_assistant: 'x-pack/plugins/observability_ai_assistant/.storybook', presentation: 'src/plugins/presentation_util/storybook', random_sampling: 'x-pack/packages/kbn-random-sampling/.storybook', text_based_editor: 'packages/kbn-text-based-editor/.storybook', diff --git a/x-pack/plugins/observability_ai_assistant/.storybook/jest_setup.js b/x-pack/plugins/observability_ai_assistant/.storybook/jest_setup.js new file mode 100644 index 0000000000000..32071b8aa3f62 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/.storybook/jest_setup.js @@ -0,0 +1,11 @@ +/* + * 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 { setGlobalConfig } from '@storybook/testing-react'; +import * as globalStorybookConfig from './preview'; + +setGlobalConfig(globalStorybookConfig); diff --git a/x-pack/plugins/observability_ai_assistant/.storybook/main.js b/x-pack/plugins/observability_ai_assistant/.storybook/main.js new file mode 100644 index 0000000000000..86b48c32f103e --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/.storybook/main.js @@ -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. + */ + +module.exports = require('@kbn/storybook').defaultConfig; diff --git a/x-pack/plugins/observability_ai_assistant/.storybook/preview.js b/x-pack/plugins/observability_ai_assistant/.storybook/preview.js new file mode 100644 index 0000000000000..3200746243d47 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/.storybook/preview.js @@ -0,0 +1,10 @@ +/* + * 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 { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common'; + +export const decorators = [EuiThemeProviderDecorator]; From 8e5e8949137e26c472e3835838ad9104abfb19b1 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 19 Jul 2023 12:24:37 +0200 Subject: [PATCH 04/23] Add AskAiAssistantButton --- .../ask_assistant_button.stories.tsx | 52 ++++++++ .../components/ask_assistant_button.tsx | 111 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx new file mode 100644 index 0000000000000..d7bbf498f0eec --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ComponentStory } from '@storybook/react'; +import { EuiButtonSize } from '@elastic/eui'; + +import { AskAssistantButton as Component, AskAssistantButtonProps } from './ask_assistant_button'; + +export default { + component: Component, + title: 'app/Atoms/AskAiAssistantButton', + argTypes: { + size: { + options: ['xs', 's', 'm'] as EuiButtonSize[], + control: { type: 'radio' }, + }, + fill: { + control: { + type: 'boolean', + }, + }, + flush: { + control: { + type: 'boolean', + if: { arg: 'variant', eq: 'empty' }, + }, + }, + variant: { + options: ['basic', 'empty', 'iconOnly'], + control: { type: 'radio' }, + }, + }, +}; + +const Template: ComponentStory = (props: AskAssistantButtonProps) => ( + +); + +const defaultProps = { + fill: true, + iconOnly: false, + size: 'm' as EuiButtonSize, + variant: 'basic' as const, +}; + +export const AskAiAssistantButton = Template.bind({}); +AskAiAssistantButton.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx new file mode 100644 index 0000000000000..04e3e21b10d03 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiButtonSize, + EuiButtonEmptySizes, + useEuiTheme, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export type AskAssistantButtonProps = ( + | { + variant: 'basic' | 'iconOnly'; + size: EuiButtonSize; + fill?: boolean; + flush?: false; + } + | { + variant: 'empty'; + size: EuiButtonEmptySizes; + fill?: false; + flush?: 'both'; + } +) & { + onClick: () => void; +}; + +// In order to leverage all the styling / display code that Eui buttons provide, +// we need to have the Sparkle icon part of EuiIcons. While we wait for that to land +// we have to redo some of that logic below. Todo: cleanup once Sparkle icon lands. + +export function AskAssistantButton({ + fill, + flush, + size, + variant, + onClick, +}: AskAssistantButtonProps) { + const contents = ( + <> + + {variant === 'empty' ? ' ' : null} + + {variant === 'iconOnly' + ? null + : i18n.translate('xpack.obsAiAssistant.askAssistantButton.buttonLabel', { + defaultMessage: 'Ask Assistant', + })} + + ); + + switch (variant) { + case 'basic': + return ( + + {contents} + + ); + + case 'empty': + return ( + + {contents} + + ); + + case 'iconOnly': + return ( + + + {contents} + + + ); + } +} + +// This icon is temporary and should be removed once it lands in Eui. +function SparkleIcon({ size, color }: { size: 'xs' | 's' | 'm'; color: 'white' | 'blue' }) { + const { euiTheme } = useEuiTheme(); + + return ( + + + + ); +} From 09f821b4a1d82e1726098239cfb7972ef3c89795 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 19 Jul 2023 14:37:46 +0200 Subject: [PATCH 05/23] Add Assistant Avatar --- .../components/assistant_avatar.stories.tsx | 33 +++++++++++++++ .../public/components/assistant_avatar.tsx | 40 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.stories.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx diff --git a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.stories.tsx new file mode 100644 index 0000000000000..3d1d770488491 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.stories.tsx @@ -0,0 +1,33 @@ +/* + * 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 { ComponentStory } from '@storybook/react'; + +import { AssistantAvatar as Component, AssistantAvatarProps } from './assistant_avatar'; + +export default { + component: Component, + title: 'app/Atoms/AssistantAvatar', + argTypes: { + size: { + options: ['xs', 's', 'm', 'l', 'xl'], + control: { type: 'radio' }, + }, + }, +}; + +const Template: ComponentStory = (props: AssistantAvatarProps) => ( + +); + +const defaultProps = { + size: 'm' as const, +}; + +export const AssistantAvatar = Template.bind({}); +AssistantAvatar.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx new file mode 100644 index 0000000000000..5d37c6d3a353f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx @@ -0,0 +1,40 @@ +/* + * 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 { EuiAvatarSize } from '@elastic/eui/src/components/avatar/avatar'; + +export interface AssistantAvatarProps { + size: EuiAvatarSize; +} + +export const sizeMap = { + xl: 64, + l: 48, + m: 32, + s: 24, + xs: 16, +}; + +export function AssistantAvatar({ size }: AssistantAvatarProps) { + return ( + + + + + + + ); +} From 378cc90da7c7a6d47191809715cdf980526bc08d Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 19 Jul 2023 14:59:43 +0200 Subject: [PATCH 06/23] First pass of InsightPanel --- .../public/components/assistant_avatar.tsx | 3 +- .../components/insight_panel.stories.tsx | 28 +++++++++++ .../public/components/insight_panel.tsx | 50 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx diff --git a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx index 5d37c6d3a353f..4d653db359cd4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx @@ -5,10 +5,9 @@ * 2.0. */ import React from 'react'; -import { EuiAvatarSize } from '@elastic/eui/src/components/avatar/avatar'; export interface AssistantAvatarProps { - size: EuiAvatarSize; + size: keyof typeof sizeMap; } export const sizeMap = { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx new file mode 100644 index 0000000000000..a90ca3b2019b0 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx @@ -0,0 +1,28 @@ +/* + * 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 { ComponentStory } from '@storybook/react'; + +import { InsightPanel as Component, InsightPanelProps } from './insight_panel'; + +export default { + component: Component, + title: 'app/Molecules/InsightPanel', + argTypes: {}, +}; + +const Template: ComponentStory = (props: InsightPanelProps) => ( + +); + +const defaultProps = { + title: 'Elastic Assistant', +}; + +export const InsightPanel = Template.bind({}); +InsightPanel.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx new file mode 100644 index 0000000000000..8d500ca19858f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx @@ -0,0 +1,50 @@ +/* + * 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; +import { AssistantAvatar } from './assistant_avatar'; + +export interface InsightPanelProps { + title: string; +} + +export function InsightPanel({ title }: InsightPanelProps) { + return ( + + + {/* expand / contract */} + + + + + {/* content */} + + + + + + + +
{title}
+
+
+
+
+ + {/* actions */} + + + + + + + +
+
+ ); +} From 9279a0ba18c429eaf38f4cfc985248c9e04f8feb Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 19 Jul 2023 15:24:51 +0200 Subject: [PATCH 07/23] newline in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index afc2bdb905e05..1a82ebc920f41 100644 --- a/package.json +++ b/package.json @@ -1546,4 +1546,4 @@ "xmlbuilder": "13.0.2", "yargs": "^15.4.1" } -} +} \ No newline at end of file From 7e2525d31756ab1505cfee77a98e9bcfdc459f2d Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 19 Jul 2023 15:29:14 +0200 Subject: [PATCH 08/23] Update AskAssistantButton to include Sparkles from Eui --- .../ask_assistant_button.stories.tsx | 1 - .../components/ask_assistant_button.tsx | 66 +++++++------------ 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx index d7bbf498f0eec..9c0d19e3c4b75 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.stories.tsx @@ -43,7 +43,6 @@ const Template: ComponentStory = (props: AskAssistantButtonPro const defaultProps = { fill: true, - iconOnly: false, size: 'm' as EuiButtonSize, variant: 'basic' as const, }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx index 04e3e21b10d03..f14b046b3caba 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx @@ -11,14 +11,14 @@ import { EuiButtonEmpty, EuiButtonSize, EuiButtonEmptySizes, - useEuiTheme, EuiToolTip, + EuiButtonIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export type AskAssistantButtonProps = ( | { - variant: 'basic' | 'iconOnly'; + variant: 'basic'; size: EuiButtonSize; fill?: boolean; flush?: false; @@ -29,14 +29,16 @@ export type AskAssistantButtonProps = ( fill?: false; flush?: 'both'; } + | { + variant: 'iconOnly'; + size: EuiButtonSize; + fill?: boolean; + flush?: false; + } ) & { onClick: () => void; }; -// In order to leverage all the styling / display code that Eui buttons provide, -// we need to have the Sparkle icon part of EuiIcons. While we wait for that to land -// we have to redo some of that logic below. Todo: cleanup once Sparkle icon lands. - export function AskAssistantButton({ fill, flush, @@ -44,31 +46,22 @@ export function AskAssistantButton({ variant, onClick, }: AskAssistantButtonProps) { - const contents = ( - <> - - {variant === 'empty' ? ' ' : null} - - {variant === 'iconOnly' - ? null - : i18n.translate('xpack.obsAiAssistant.askAssistantButton.buttonLabel', { - defaultMessage: 'Ask Assistant', - })} - - ); + const buttonLabel = i18n.translate('xpack.obsAiAssistant.askAssistantButton.buttonLabel', { + defaultMessage: 'Ask Assistant', + }); switch (variant) { case 'basic': return ( - - {contents} + + {buttonLabel} ); case 'empty': return ( - - {contents} + + {buttonLabel} ); @@ -83,29 +76,14 @@ export function AskAssistantButton({ defaultMessage: 'Get insights into your data with the Elastic Assistant', })} > - - {contents} - + ); } } - -// This icon is temporary and should be removed once it lands in Eui. -function SparkleIcon({ size, color }: { size: 'xs' | 's' | 'm'; color: 'white' | 'blue' }) { - const { euiTheme } = useEuiTheme(); - - return ( - - - - ); -} From 4fc20ac099ff0cdac291e273f188b85918834ada Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:31:28 +0000 Subject: [PATCH 09/23] [CI] Auto-commit changed files from 'node scripts/lint_ts_projects --fix' --- x-pack/plugins/observability_ai_assistant/tsconfig.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index f723c0bc7a9d0..884e2929f3773 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -19,7 +19,9 @@ "@kbn/logging", "@kbn/triggers-actions-ui-plugin", "@kbn/config-schema", - "@kbn/security-plugin" + "@kbn/security-plugin", + "@kbn/i18n", + "@kbn/spaces-plugin" ], "exclude": [ "target/**/*", From b06714b8b83c23c8ea52d68197ccfb705df3a497 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:38:38 +0000 Subject: [PATCH 10/23] [CI] Auto-commit changed files from 'node scripts/generate codeowners' --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f80c87ff4058b..09190bbde7d27 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -510,6 +510,7 @@ src/plugins/newsfeed @elastic/kibana-core test/common/plugins/newsfeed @elastic/kibana-core x-pack/plugins/notifications @elastic/appex-sharedux packages/kbn-object-versioning @elastic/appex-sharedux +x-pack/plugins/observability_ai_assistant @elastic/apm-ui x-pack/packages/observability/alert_details @elastic/actionable-observability x-pack/test/cases_api_integration/common/plugins/observability @elastic/response-ops x-pack/plugins/observability_onboarding @elastic/apm-ui From 0cd6dde87fa0ad401f8860072576036a89538172 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:46:31 +0000 Subject: [PATCH 11/23] [CI] Auto-commit changed files from 'node scripts/build_plugin_list_docs' --- docs/developer/plugin-list.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e29f0fa2ac0ed..701a0915f8d2f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -657,6 +657,10 @@ Elastic. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. +|{kib-repo}blob/{branch}/x-pack/plugins/observability_ai_assistant/README.md[observabilityAIAssistant] +|This plugin provides the Observability AI Assistant service and UI components. + + |{kib-repo}blob/{branch}/x-pack/plugins/observability_onboarding/README.md[observabilityOnboarding] |This plugin provides an onboarding framework for observability solutions: Logs and APM. From e27e5907b38d53cae1da3a7c9b090e983750df3d Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 19 Jul 2023 16:08:45 +0200 Subject: [PATCH 12/23] Add plugin to limits.yml --- packages/kbn-optimizer/limits.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index ac968b0d84dcc..5f1d9dddfafdc 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -97,6 +97,7 @@ pageLoadAssetSize: navigation: 37269 newsfeed: 42228 observability: 115443 + observabilityAIAssistant: 16759 observabilityOnboarding: 19573 observabilityShared: 52256 osquery: 107090 From 8a30a08ccaa86c5d1524b5ba6629c275166eff5a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 19 Jul 2023 17:48:13 +0200 Subject: [PATCH 13/23] Fix i18n check --- x-pack/.i18nrc.json | 1 + .../public/components/ask_assistant_button.tsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6fbc0f935b940..91c0a9993fbf9 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -60,6 +60,7 @@ "xpack.observability": "plugins/observability", "xpack.observabilityShared": "plugins/observability_shared", "xpack.observability_onboarding": "plugins/observability_onboarding", + "xpack.observabilityAiAssistant": "plugins/observability_ai_assistant", "xpack.osquery": ["plugins/osquery"], "xpack.painlessLab": "plugins/painless_lab", "xpack.profiling": ["plugins/profiling"], diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx index 04e3e21b10d03..7ec3a30a1ef3e 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx @@ -51,7 +51,7 @@ export function AskAssistantButton({ {variant === 'iconOnly' ? null - : i18n.translate('xpack.obsAiAssistant.askAssistantButton.buttonLabel', { + : i18n.translate('xpack.observabilityAiAssistant.askAssistantButton.buttonLabel', { defaultMessage: 'Ask Assistant', })} @@ -76,12 +76,15 @@ export function AskAssistantButton({ return ( {contents} From a9c59b6b6c20b5adacecfbd7672605ca7e99001a Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 19 Jul 2023 22:38:01 +0200 Subject: [PATCH 14/23] Added Insight component --- .../public/components/feedback_buttons.tsx | 62 +++++++ .../components/insight/insight.stories.tsx | 42 +++++ .../public/components/insight/insight.tsx | 171 ++++++++++++++++++ .../components/insight/insight_error.tsx | 34 ++++ .../insight/insight_generated_response.tsx | 79 ++++++++ .../insight/insight_missing_credentials.tsx | 35 ++++ .../components/insight/use_streaming_words.ts | 28 +++ .../components/insight_panel.stories.tsx | 28 --- .../public/components/insight_panel.tsx | 50 ----- .../public/utils/storybook_decorator.tsx | 27 +++ 10 files changed, 478 insertions(+), 78 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx diff --git a/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx b/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx new file mode 100644 index 0000000000000..2e960e5d771e6 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/feedback_buttons.tsx @@ -0,0 +1,62 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +export type Feedback = 'positive' | 'negative'; + +interface FeedbackButtonsProps { + onClickFeedback: (feedback: Feedback) => void; +} + +export function FeedbackButtons({ onClickFeedback }: FeedbackButtonsProps) { + return ( + + + + + {i18n.translate('xpack.obsAiAssistant.insight.feedbackButtons.title', { + defaultMessage: 'Was this helpful?', + })} + + + + + + + + onClickFeedback('positive')} + > + {i18n.translate('xpack.obsAiAssistant.insight.feedbackButtons.positive', { + defaultMessage: 'Yes', + })} + + + + + onClickFeedback('negative')} + > + {i18n.translate('xpack.obsAiAssistant.insight.feedbackButtons.negative', { + defaultMessage: 'No', + })} + + + + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx new file mode 100644 index 0000000000000..8ca01d2f2588c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx @@ -0,0 +1,42 @@ +/* + * 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 { ComponentStory } from '@storybook/react'; + +import { Insight as Component, InsightProps } from './insight'; +import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; + +export default { + component: Component, + title: 'app/Molecules/Insight', + argTypes: { + debug: { + control: { + type: 'boolean', + }, + }, + }, + decorators: [KibanaReactStorybookDecorator], +}; + +const Template: ComponentStory = (props: InsightProps) => ( + +); + +const defaultProps = { + title: 'Elastic Assistant', + actions: [ + { id: 'foo', label: 'Put hands in pockets', handler: () => {} }, + { id: 'bar', label: 'Drop kick', handler: () => {} }, + ], + description: 'What is the root cause of performance degradation in my service?', + debug: true, +}; + +export const Insight = Template.bind({}); +Insight.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx new file mode 100644 index 0000000000000..cc16f02be5f36 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx @@ -0,0 +1,171 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiAccordion, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import moment from 'moment'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { AssistantAvatar } from '../assistant_avatar'; +import { InsightMissingCredentials } from './insight_missing_credentials'; +import { InsightError } from './insight_error'; +import { InsightGeneratedResponse } from './insight_generated_response'; +import { Feedback } from '../feedback_buttons'; + +export interface InsightProps { + title: string; + description?: string; + date?: Date; + debug?: boolean; + actions: Array<{ id: string; label: string; icon?: string; handler: () => void }>; +} + +export function Insight({ + title, + description, + date = new Date(), + debug, + actions = [], +}: InsightProps) { + const { euiTheme } = useEuiTheme(); + const { uiSettings } = useKibana().services; + + const dateFormat = uiSettings?.get('dateFormat'); + + const [isActionsPopoverOpen, setIsActionsPopover] = useState(false); + + const [state, setState] = useState<'missing' | 'error' | 'insightGenerated'>('insightGenerated'); + + const handleClickActions = () => { + setIsActionsPopover(!isActionsPopoverOpen); + }; + + const handleFeedback = (feedback: Feedback) => {}; + + const handleRegenerate = () => {}; + + const handleStartChat = () => {}; + + return ( + + + + + + + + +
{title}
+
+ + + {description} + + + + + + + {i18n.translate('xpack.obsAiAssistant.insight.generatedAt', { + defaultMessage: 'Generated at', + })}{' '} + {moment(date).format(dateFormat)} + + +
+ + } + extraAction={ + + } + panelPaddingSize="s" + closePopover={handleClickActions} + isOpen={isActionsPopoverOpen} + > + ( + + {label} + + ))} + /> + + } + > + + + {/* Debug controls. */} + {debug ? ( + + setState('insightGenerated')} + > + Normal + + setState('missing')} + > + Missing credentials + + setState('error')}> + Error + + + ) : null} + + {state === 'insightGenerated' ? ( + + ) : null} + + {state === 'error' ? : null} + + {state === 'missing' ? : null} +
+
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx new file mode 100644 index 0000000000000..475d0df97eaa9 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_error.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function InsightError() { + return ( + + {i18n.translate('xpack.obsAiAssistant.insight.error.description', { + defaultMessage: 'An error occured.', + })} + + + + + {i18n.translate('xpack.obsAiAssistant.insight.error.buttonLabel', { + defaultMessage: 'Regenerate', + })} + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx new file mode 100644 index 0000000000000..214ded3875706 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Feedback, FeedbackButtons } from '../feedback_buttons'; +import { useStreamingText } from './use_streaming_words'; +interface InsightGeneratedResponseProps { + answer: string; + onClickFeedback: (feedback: Feedback) => void; + onClickRegenerate: () => void; + onClickStartChat: () => void; +} + +export function InsightGeneratedResponse({ + onClickFeedback, + onClickRegenerate, + onClickStartChat, +}: InsightGeneratedResponseProps) { + const answer = useStreamingText({ + message: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +Aliquam commodo sollicitudin erat in ultrices. Vestibulum euismod ex ac lectus semper hendrerit. + +Morbi mattis odio justo, in ullamcorper metus aliquet eu. Praesent risus velit, rutrum ac magna non, vehicula vestibulum sapien. Quisque pulvinar eros eu finibus iaculis. + +Morbi dapibus sapien lacus, vitae suscipit ex egestas pharetra. In velit eros, fermentum sit amet augue ut, aliquam sodales nulla. Nunc mattis lobortis eros sit amet dapibus. + + Morbi non faucibus massa. Aliquam sed augue in eros ornare luctus sit amet cursus dolor. Pellentesque pellentesque lorem eu odio auctor convallis. Sed sodales felis at velit tempus tincidunt. Nulla sed ante cursus nibh mollis blandit. In mattis imperdiet tellus. Vestibulum nisl turpis, efficitur quis sollicitudin id, mollis in arcu. Vestibulum pulvinar tincidunt magna, vitae facilisis massa congue quis. Cras commodo efficitur tellus, et commodo risus rutrum at.`, + }); + return ( + + +

{answer}

+
+ + + + + + + + + + + + + {i18n.translate('xpack.obsAiAssistant.insight.response.regenerate', { + defaultMessage: 'Regenerate', + })} + + + + + + {i18n.translate('xpack.obsAiAssistant.insight.response.startChat', { + defaultMessage: 'Start chat', + })} + + + + + +
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx new file mode 100644 index 0000000000000..c2c7305c2e303 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function InsightMissingCredentials() { + return ( + + {i18n.translate('xpack.obsAiAssistant.insight.missing.description', { + defaultMessage: + 'You haven’t authorised OpenAI in order to generate responses from the Elastic Assistant. Authorise the model in order to proceed.', + })} + + + + + {i18n.translate('xpack.obsAiAssistant.insight.missing.buttonLabel', { + defaultMessage: 'Connect Assistant', + })} + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts b/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts new file mode 100644 index 0000000000000..4bd4cedd1bff0 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts @@ -0,0 +1,28 @@ +/* + * 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 { useEffect, useState } from 'react'; + +interface UseStreamingTextProps { + message: string; +} + +export function useStreamingText({ message }: UseStreamingTextProps) { + const [chatMessages, setChatMessages] = useState(''); + + useEffect(() => { + const words = message.split(' '); + + for (let i = 0; i < words.length; i++) { + setTimeout(() => { + setChatMessages((prevState) => `${prevState} ${words[i]}`); + }, i * 50); // Adjust typing speed here (milliseconds per word) + } + }, [message]); + + return chatMessages; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx deleted file mode 100644 index a90ca3b2019b0..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.stories.tsx +++ /dev/null @@ -1,28 +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 { ComponentStory } from '@storybook/react'; - -import { InsightPanel as Component, InsightPanelProps } from './insight_panel'; - -export default { - component: Component, - title: 'app/Molecules/InsightPanel', - argTypes: {}, -}; - -const Template: ComponentStory = (props: InsightPanelProps) => ( - -); - -const defaultProps = { - title: 'Elastic Assistant', -}; - -export const InsightPanel = Template.bind({}); -InsightPanel.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx deleted file mode 100644 index 8d500ca19858f..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight_panel.tsx +++ /dev/null @@ -1,50 +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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; -import { AssistantAvatar } from './assistant_avatar'; - -export interface InsightPanelProps { - title: string; -} - -export function InsightPanel({ title }: InsightPanelProps) { - return ( - - - {/* expand / contract */} - - - - - {/* content */} - - - - - - - -
{title}
-
-
-
-
- - {/* actions */} - - - - - - - -
-
- ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx new file mode 100644 index 0000000000000..0ef1cfa5ce358 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/storybook_decorator.tsx @@ -0,0 +1,27 @@ +/* + * 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, { ComponentType } from 'react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +export function KibanaReactStorybookDecorator(Story: ComponentType) { + return ( + { + if (setting === 'dateFormat') { + return 'MMM D, YYYY HH:mm'; + } + }, + }, + }} + > + + + ); +} From d391008667c8d26fc0fc47d146c7523769b0c17f Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 20 Jul 2023 10:26:34 +0200 Subject: [PATCH 15/23] Update i18n translation base key --- .../components/ask_assistant_button.tsx | 20 ++++++++++++------- .../public/components/feedback_buttons.tsx | 6 +++--- .../public/components/insight/insight.tsx | 4 ++-- .../components/insight/insight_error.tsx | 6 +++--- .../insight/insight_generated_response.tsx | 4 ++-- .../insight/insight_missing_credentials.tsx | 6 +++--- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx index f14b046b3caba..cbd95b9b80fe5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/ask_assistant_button.tsx @@ -46,9 +46,12 @@ export function AskAssistantButton({ variant, onClick, }: AskAssistantButtonProps) { - const buttonLabel = i18n.translate('xpack.obsAiAssistant.askAssistantButton.buttonLabel', { - defaultMessage: 'Ask Assistant', - }); + const buttonLabel = i18n.translate( + 'xpack.observabilityAiAssistant.askAssistantButton.buttonLabel', + { + defaultMessage: 'Ask Assistant', + } + ); switch (variant) { case 'basic': @@ -69,12 +72,15 @@ export function AskAssistantButton({ return ( - {i18n.translate('xpack.obsAiAssistant.insight.feedbackButtons.title', { + {i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.title', { defaultMessage: 'Was this helpful?', })} @@ -37,7 +37,7 @@ export function FeedbackButtons({ onClickFeedback }: FeedbackButtonsProps) { size="s" onClick={() => onClickFeedback('positive')} > - {i18n.translate('xpack.obsAiAssistant.insight.feedbackButtons.positive', { + {i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.positive', { defaultMessage: 'Yes', })}
@@ -50,7 +50,7 @@ export function FeedbackButtons({ onClickFeedback }: FeedbackButtonsProps) { size="s" onClick={() => onClickFeedback('negative')} > - {i18n.translate('xpack.obsAiAssistant.insight.feedbackButtons.negative', { + {i18n.translate('xpack.observabilityAiAssistant.insight.feedbackButtons.negative', { defaultMessage: 'No', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx index cc16f02be5f36..56c0126907f68 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.tsx @@ -88,7 +88,7 @@ export function Insight({ - {i18n.translate('xpack.obsAiAssistant.insight.generatedAt', { + {i18n.translate('xpack.observabilityAiAssistant.insight.generatedAt', { defaultMessage: 'Generated at', })}{' '} {moment(date).format(dateFormat)} @@ -102,7 +102,7 @@ export function Insight({ anchorPosition="downLeft" button={ - {i18n.translate('xpack.obsAiAssistant.insight.error.description', { + {i18n.translate('xpack.observabilityAiAssistant.insight.error.description', { defaultMessage: 'An error occured.', })} - {i18n.translate('xpack.obsAiAssistant.insight.error.buttonLabel', { + {i18n.translate('xpack.observabilityAiAssistant.insight.error.buttonLabel', { defaultMessage: 'Regenerate', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx index 214ded3875706..b396899849240 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx @@ -58,7 +58,7 @@ Morbi dapibus sapien lacus, vitae suscipit ex egestas pharetra. In velit eros, f - {i18n.translate('xpack.obsAiAssistant.insight.response.regenerate', { + {i18n.translate('xpack.observabilityAiAssistant.insight.response.regenerate', { defaultMessage: 'Regenerate', })} @@ -66,7 +66,7 @@ Morbi dapibus sapien lacus, vitae suscipit ex egestas pharetra. In velit eros, f - {i18n.translate('xpack.obsAiAssistant.insight.response.startChat', { + {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { defaultMessage: 'Start chat', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx index c2c7305c2e303..e074abe7830c5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_missing_credentials.tsx @@ -12,13 +12,13 @@ import { i18n } from '@kbn/i18n'; export function InsightMissingCredentials() { return ( - {i18n.translate('xpack.obsAiAssistant.insight.missing.description', { + {i18n.translate('xpack.observabilityAiAssistant.insight.missing.description', { defaultMessage: 'You haven’t authorised OpenAI in order to generate responses from the Elastic Assistant. Authorise the model in order to proceed.', })} @@ -26,7 +26,7 @@ export function InsightMissingCredentials() { - {i18n.translate('xpack.obsAiAssistant.insight.missing.buttonLabel', { + {i18n.translate('xpack.observabilityAiAssistant.insight.missing.buttonLabel', { defaultMessage: 'Connect Assistant', })} From 3c96673fac0e4ba91a21b1c998f512c620d6a807 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 20 Jul 2023 16:50:35 +0200 Subject: [PATCH 16/23] Add builders for Messages --- .../public/utils/builders.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/public/utils/builders.ts diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts new file mode 100644 index 0000000000000..2c34657fa07bc --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts @@ -0,0 +1,69 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { Message, MessageRole } from '../../common/types'; + +const baseMessage: Message = { + '@timestamp': String(new Date().getUTCMilliseconds()), + message: { + content: 'foo', + name: 'bar', + role: MessageRole.User, + }, +}; + +export function buildMessage(params: Partial = {}): Message { + return cloneDeep({ ...baseMessage, ...params }); +} + +export function buildUserMessage(params: Partial = {}): Message { + return cloneDeep({ + ...baseMessage, + ...{ message: { content: "What's this function?", role: MessageRole.User } }, + ...params, + }); +} + +export function buildAssistantMessage(params: Partial = {}): Message { + return cloneDeep({ + ...baseMessage, + ...{ + message: { + content: 'This is "leftpad":', + role: MessageRole.Assistant, + data: { key: 'value', nestedData: { foo: 'bar' } }, + }, + }, + ...params, + }); +} + +export function buildElasticMessage(params: Partial = {}): Message { + return cloneDeep({ + ...baseMessage, + ...{ + message: { role: MessageRole.Elastic, data: { key: 'value', nestedData: { foo: 'bar' } } }, + }, + ...params, + }); +} + +export function buildFunctionMessage(params: Partial = {}): Message { + return cloneDeep({ + ...baseMessage, + ...{ + message: { role: MessageRole.Function }, + function_call: { + name: 'leftpad', + args: '{ foo: "bar" }', + trigger: MessageRole.User, + }, + }, + ...params, + }); +} From c0decc669373fa0c13bcba4695a39d23bd7a8019 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 20 Jul 2023 16:51:23 +0200 Subject: [PATCH 17/23] Add hooks --- .../__storybook_mocks__/use_current_user.ts | 29 ++++++++++++++++++ .../public/hooks/use_current_user.ts | 30 +++++++++++++++++++ .../public/hooks/use_kibana.ts | 18 +++++++++++ .../insight => hooks}/use_streaming_words.ts | 0 4 files changed, 77 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_current_user.ts create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_kibana.ts rename x-pack/plugins/observability_ai_assistant/public/{components/insight => hooks}/use_streaming_words.ts (100%) diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts new file mode 100644 index 0000000000000..bf9840151ef02 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +interface User { + username: string; + email?: string; + full_name?: string; + roles: readonly string[]; + enabled: boolean; + metadata?: { + _reserved: boolean; + _deprecated?: boolean; + _deprecated_reason?: string; + }; +} + +export function useCurrentUser() { + return { + username: 'john_doe', + email: 'john.doe@example.com', + full_name: 'John Doe', + roles: ['user', 'editor'], + enabled: true, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_current_user.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_current_user.ts new file mode 100644 index 0000000000000..14098e8fe09b5 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_current_user.ts @@ -0,0 +1,30 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { AuthenticatedUser } from '@kbn/security-plugin/common/model'; +import { useKibana } from './use_kibana'; + +export function useCurrentUser() { + const { security } = useKibana().services; + + const [user, setUser] = useState(); + + useEffect(() => { + const getCurrentUser = async () => { + try { + const authenticatedUser = await security?.authc.getCurrentUser(); + setUser(authenticatedUser); + } catch { + setUser(undefined); + } + }; + getCurrentUser(); + }, [security?.authc]); + + return user; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_kibana.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_kibana.ts new file mode 100644 index 0000000000000..f2f812654a2e0 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_kibana.ts @@ -0,0 +1,18 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../types'; + +export type StartServices = CoreStart & + ObservabilityAIAssistantPluginStartDependencies & + AdditionalServices & {}; +const useTypedKibana = () => + useKibana>(); + +export { useTypedKibana as useKibana }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_streaming_words.ts similarity index 100% rename from x-pack/plugins/observability_ai_assistant/public/components/insight/use_streaming_words.ts rename to x-pack/plugins/observability_ai_assistant/public/hooks/use_streaming_words.ts From 1a3d74c09a5dbace0ff3bcb9115e04837349631c Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 20 Jul 2023 16:52:18 +0200 Subject: [PATCH 18/23] Add security as dependency --- x-pack/plugins/observability_ai_assistant/kibana.jsonc | 10 ++-------- .../plugins/observability_ai_assistant/public/types.ts | 5 +++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index c3a1dcd41a270..3c226478d87fe 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -6,14 +6,8 @@ "id": "observabilityAIAssistant", "server": true, "browser": true, - "configPath": [ - "xpack", - "observabilityAIAssistant" - ], - "requiredPlugins": [ - "triggersActionsUi", - "actions" - ], + "configPath": ["xpack", "observabilityAIAssistant"], + "requiredPlugins": ["actions", "spaces", "triggersActionsUi"], "optionalPlugins": [], "extraPublicDirs": [] } diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 68f18af530f8f..1a960ca27f4a0 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, @@ -12,11 +13,15 @@ import type { /* eslint-disable @typescript-eslint/no-empty-interface*/ export interface ObservabilityAIAssistantPluginStart {} + export interface ObservabilityAIAssistantPluginSetup {} + export interface ObservabilityAIAssistantPluginSetupDependencies { triggersActions: TriggersAndActionsUIPublicPluginSetup; } + export interface ObservabilityAIAssistantPluginStartDependencies { + security: SecurityPluginStart; triggersActions: TriggersAndActionsUIPublicPluginStart; } From 990d6d11e4a7e0cf4d7409acbb84f02306d95aa4 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 20 Jul 2023 16:52:47 +0200 Subject: [PATCH 19/23] Update imports --- .../public/components/assistant_avatar.tsx | 7 ++++--- .../public/components/insight/insight.tsx | 2 +- .../components/insight/insight_generated_response.tsx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx index 4d653db359cd4..422bd42f16c3f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; export interface AssistantAvatarProps { - size: keyof typeof sizeMap; + size?: keyof typeof sizeMap; + children?: ReactNode; } export const sizeMap = { @@ -18,7 +19,7 @@ export const sizeMap = { xs: 16, }; -export function AssistantAvatar({ size }: AssistantAvatarProps) { +export function AssistantAvatar({ size = 's' }: AssistantAvatarProps) { return ( void; From fa107667987ca2eb97c1b89918b4162cfcad858d Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Thu, 20 Jul 2023 16:53:05 +0200 Subject: [PATCH 20/23] First pass of Chat interface --- .../public/components/chat/chat_avatar.tsx | 37 ++++++ .../components/chat/chat_timeline.stories.tsx | 34 ++++++ .../public/components/chat/chat_timeline.tsx | 109 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx new file mode 100644 index 0000000000000..5aae9db530af8 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx @@ -0,0 +1,37 @@ +/* + * 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 { UserAvatar } from '@kbn/user-profile-components'; +import { EuiAvatar, EuiLoadingSpinner } from '@elastic/eui'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { AssistantAvatar } from '../assistant_avatar'; +import { MessageRole } from '../../../common/types'; + +interface ChatAvatarProps { + role: MessageRole; + user?: AuthenticatedUser | undefined; +} + +export function ChatAvatar({ user, role }: ChatAvatarProps) { + switch (role) { + case MessageRole.User: + return user ? ( + + ) : ( + + ); + + case MessageRole.Assistant: + case MessageRole.Elastic: + case MessageRole.Function: + return ; + + default: + return null; + } +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx new file mode 100644 index 0000000000000..b145b56866611 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ComponentStory } from '@storybook/react'; + +import { ChatTimeline as Component, ChatTimelineProps } from './chat_timeline'; +import { buildAssistantMessage, buildElasticMessage, buildUserMessage } from '../../utils/builders'; + +export default { + component: Component, + title: 'app/Molecules/ChatTimeline', + argTypes: {}, +}; + +const Template: ComponentStory = (props: ChatTimelineProps) => ( + +); + +const defaultProps = { + messages: [ + buildUserMessage(), + buildAssistantMessage(), + buildUserMessage(), + buildElasticMessage(), + ], +}; + +export const ChatTimeline = Template.bind({}); +ChatTimeline.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx new file mode 100644 index 0000000000000..e160add8e7b91 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -0,0 +1,109 @@ +/* + * 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 { EuiText, EuiCommentList, EuiComment, EuiCode, EuiPanel } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Message, MessageRole } from '../../../common/types'; +import { useCurrentUser } from '../../hooks/use_current_user'; +import { ChatAvatar } from './chat_avatar'; + +export interface ChatTimelineProps { + messages: Message[]; +} + +export function ChatTimeline({ messages = [] }: ChatTimelineProps) { + const currentUser = useCurrentUser(); + + return ( + + {messages.map((message, index) => ( + } + > + {message.message.role === MessageRole.User && index === 0 ? ( + +

{message.message.content}

+
+ ) : ( + +

{message.message.content}

+
+ )} + + ))} + + ); +} + +// +// {messages.map((message, index) => ( +// } +// > +// +// {message.message.role === MessageRole.User && index === 0 ? ( +// <> +// +// +//

+// +// {i18n.translate( +// 'xpack.observabilityAiAssistant.chatTimeline.messages.userInitiatedTitle.you', +// { defaultMessage: 'You' } +// )}{' '} +// +// {i18n.translate( +// 'xpack.observabilityAiAssistant.chatTimeline.messages.userInitiatedTitle.createdNewConversation', +// { +// defaultMessage: 'created a new conversation', +// } +// )} +//

+//
+//
+// +// +// ) : null} + +// +// +//

{message.message.content}

+//
+//
+//
+//
+// ))} +//
From 0b66523b5249b4319ffcdd6aabc6eca6f91b2b31 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 25 Jul 2023 10:52:33 +0200 Subject: [PATCH 21/23] Add chat timeline --- .../public/components/chat/chat_item.tsx | 65 ++++++++++++ .../{chat_avatar.tsx => chat_item_avatar.tsx} | 13 ++- .../components/chat/chat_item_title.tsx | 68 +++++++++++++ .../components/chat/chat_timeline.stories.tsx | 63 ++++++++++-- .../public/components/chat/chat_timeline.tsx | 98 +++---------------- .../__storybook_mocks__/use_current_user.ts | 13 --- .../hooks/__storybook_mocks__/use_kibana.ts | 20 ++++ .../public/utils/builders.ts | 60 +++++++----- 8 files changed, 261 insertions(+), 139 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx rename x-pack/plugins/observability_ai_assistant/public/components/chat/{chat_avatar.tsx => chat_item_avatar.tsx} (71%) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx new file mode 100644 index 0000000000000..b1ad9916dfc82 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx @@ -0,0 +1,65 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiText, EuiComment } from '@elastic/eui'; +import type { AuthenticatedUser } from '@kbn/security-plugin/common'; +import { MessageRole, Message } from '../../../common/types'; +import { ChatItemAvatar } from './chat_item_avatar'; +import { ChatItemTitle } from './chat_item_title'; + +const roleMap = { + [MessageRole.User]: i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.userLabel', + { defaultMessage: 'You' } + ), + [MessageRole.System]: i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.systemLabel', + { defaultMessage: 'System' } + ), + [MessageRole.Assistant]: i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.assistantLabel', + { defaultMessage: 'Elastic Assistant' } + ), + [MessageRole.Function]: i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.functionLabel', + { defaultMessage: 'Elastic Assistant' } + ), + [MessageRole.Event]: i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.functionLabel', + { defaultMessage: 'Elastic Assistant' } + ), + [MessageRole.Elastic]: i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.functionLabel', + { defaultMessage: 'Elastic Assistant' } + ), +}; + +export interface ChatItemProps { + currentUser: AuthenticatedUser | undefined; + dateFormat: string; + index: number; + message: Message; +} + +export function ChatItem({ currentUser, dateFormat, index, message }: ChatItemProps) { + return ( + } + timelineAvatar={} + username={roleMap[message.message.role]} + > + {message.message.content ? ( + +

{message.message.content}

+
+ ) : null} +
+ ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_avatar.tsx similarity index 71% rename from x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx rename to x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_avatar.tsx index 5aae9db530af8..0fb624bab8908 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_avatar.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_avatar.tsx @@ -13,15 +13,15 @@ import { AssistantAvatar } from '../assistant_avatar'; import { MessageRole } from '../../../common/types'; interface ChatAvatarProps { + currentUser?: AuthenticatedUser | undefined; role: MessageRole; - user?: AuthenticatedUser | undefined; } -export function ChatAvatar({ user, role }: ChatAvatarProps) { +export function ChatItemAvatar({ currentUser, role }: ChatAvatarProps) { switch (role) { case MessageRole.User: - return user ? ( - + return currentUser ? ( + ) : ( ); @@ -29,7 +29,10 @@ export function ChatAvatar({ user, role }: ChatAvatarProps) { case MessageRole.Assistant: case MessageRole.Elastic: case MessageRole.Function: - return ; + return ; + + case MessageRole.System: + return ; default: return null; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx new file mode 100644 index 0000000000000..393886fbef800 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item_title.tsx @@ -0,0 +1,68 @@ +/* + * 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 moment from 'moment'; +import { i18n } from '@kbn/i18n'; +import { Message, MessageRole } from '../../../common/types'; + +interface ChatItemTitleProps { + dateFormat: string; + index: number; + message: Message; +} + +export function ChatItemTitle({ dateFormat, index, message }: ChatItemTitleProps) { + switch (message.message.role) { + case MessageRole.User: + if (index === 0) { + return i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.user.createdNewConversation', + { + defaultMessage: 'created a new conversation on {date}', + values: { + date: moment(message['@timestamp']).format(dateFormat), + }, + } + ); + } else { + return i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.user.addedPrompt', + { + defaultMessage: 'added a prompt on {date}', + values: { + date: moment(message['@timestamp']).format(dateFormat), + }, + } + ); + } + + case MessageRole.Assistant: + case MessageRole.Elastic: + case MessageRole.Function: + return i18n.translate( + 'xpack.observabilityAiAssistant.chatTimeline.messages.elasticAssistant.responded', + { + defaultMessage: 'responded on {date}', + values: { + date: moment(message['@timestamp']).format(dateFormat), + }, + } + ); + + case MessageRole.System: + return i18n.translate('xpack.observabilityAiAssistant.chatTimeline.messages.system.added', { + defaultMessage: 'added {thing} on {date}', + values: { + date: moment(message['@timestamp']).format(dateFormat), + thing: message.message.content, + }, + }); + + default: + return ''; + } +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx index b145b56866611..dea53f07da744 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.stories.tsx @@ -5,28 +5,73 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { ComponentStory } from '@storybook/react'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import { ChatTimeline as Component, ChatTimelineProps } from './chat_timeline'; -import { buildAssistantMessage, buildElasticMessage, buildUserMessage } from '../../utils/builders'; +import { + buildAssistantInnerMessage, + buildElasticInnerMessage, + buildMessage, + buildSystemInnerMessage, + buildUserInnerMessage, +} from '../../utils/builders'; export default { component: Component, title: 'app/Molecules/ChatTimeline', + parameters: { + backgrounds: { + default: 'white', + values: [{ name: 'white', value: '#fff' }], + }, + }, argTypes: {}, }; -const Template: ComponentStory = (props: ChatTimelineProps) => ( - -); +const Template: ComponentStory = (props: ChatTimelineProps) => { + const [count, setCount] = useState(0); + + return ( + <> + index <= count)} /> + + + + setCount(count >= 0 && count < props.messages.length - 1 ? count + 1 : 0)} + > + Add message + + + ); +}; + +const currentDate = new Date(); const defaultProps = { messages: [ - buildUserMessage(), - buildAssistantMessage(), - buildUserMessage(), - buildElasticMessage(), + buildMessage({ + '@timestamp': String(new Date(currentDate.getTime())), + message: buildSystemInnerMessage(), + }), + buildMessage({ + '@timestamp': String(new Date(currentDate.getTime() + 1000)), + message: buildUserInnerMessage(), + }), + buildMessage({ + '@timestamp': String(new Date(currentDate.getTime() + 2000)), + message: buildAssistantInnerMessage(), + }), + buildMessage({ + '@timestamp': String(new Date(currentDate.getTime() + 3000)), + message: buildUserInnerMessage({ content: 'How does it work?' }), + }), + buildMessage({ + '@timestamp': String(new Date(currentDate.getTime() + 4000)), + message: buildElasticInnerMessage({ content: 'Here you go.' }), + }), ], }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx index e160add8e7b91..0576349a16b48 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -6,104 +6,32 @@ */ import React from 'react'; -import { EuiText, EuiCommentList, EuiComment, EuiCode, EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Message, MessageRole } from '../../../common/types'; +import { EuiCommentList } from '@elastic/eui'; +import { useKibana } from '../../hooks/use_kibana'; import { useCurrentUser } from '../../hooks/use_current_user'; -import { ChatAvatar } from './chat_avatar'; +import { Message } from '../../../common/types'; +import { ChatItem } from './chat_item'; export interface ChatTimelineProps { messages: Message[]; } export function ChatTimeline({ messages = [] }: ChatTimelineProps) { + const { uiSettings } = useKibana().services; const currentUser = useCurrentUser(); + const dateFormat = uiSettings?.get('dateFormat'); + return ( {messages.map((message, index) => ( - } - > - {message.message.role === MessageRole.User && index === 0 ? ( - -

{message.message.content}

-
- ) : ( - -

{message.message.content}

-
- )} -
+ ))}
); } - -// -// {messages.map((message, index) => ( -// } -// > -// -// {message.message.role === MessageRole.User && index === 0 ? ( -// <> -// -// -//

-// -// {i18n.translate( -// 'xpack.observabilityAiAssistant.chatTimeline.messages.userInitiatedTitle.you', -// { defaultMessage: 'You' } -// )}{' '} -// -// {i18n.translate( -// 'xpack.observabilityAiAssistant.chatTimeline.messages.userInitiatedTitle.createdNewConversation', -// { -// defaultMessage: 'created a new conversation', -// } -// )} -//

-//
-//
-// -// -// ) : null} - -// -// -//

{message.message.content}

-//
-//
-//
-//
-// ))} -//
diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts index bf9840151ef02..cbeba226f3835 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_current_user.ts @@ -5,19 +5,6 @@ * 2.0. */ -interface User { - username: string; - email?: string; - full_name?: string; - roles: readonly string[]; - enabled: boolean; - metadata?: { - _reserved: boolean; - _deprecated?: boolean; - _deprecated_reason?: string; - }; -} - export function useCurrentUser() { return { username: 'john_doe', diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts new file mode 100644 index 0000000000000..8fdfe6d29c20b --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/__storybook_mocks__/use_kibana.ts @@ -0,0 +1,20 @@ +/* + * 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 function useKibana() { + return { + services: { + uiSettings: { + get: (setting: string) => { + if (setting === 'dateFormat') { + return 'MMM D, YYYY HH:mm'; + } + }, + }, + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts index 2c34657fa07bc..26206802fca4e 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts +++ b/x-pack/plugins/observability_ai_assistant/public/utils/builders.ts @@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash'; import { Message, MessageRole } from '../../common/types'; const baseMessage: Message = { - '@timestamp': String(new Date().getUTCMilliseconds()), + '@timestamp': String(Date.now()), message: { content: 'foo', name: 'bar', @@ -21,48 +21,54 @@ export function buildMessage(params: Partial = {}): Message { return cloneDeep({ ...baseMessage, ...params }); } -export function buildUserMessage(params: Partial = {}): Message { +export function buildSystemInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, - ...{ message: { content: "What's this function?", role: MessageRole.User } }, - ...params, + ...{ role: MessageRole.System, ...params }, }); } -export function buildAssistantMessage(params: Partial = {}): Message { +export function buildUserInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, - ...{ - message: { - content: 'This is "leftpad":', - role: MessageRole.Assistant, - data: { key: 'value', nestedData: { foo: 'bar' } }, - }, - }, - ...params, + ...{ content: "What's this function?", role: MessageRole.User, ...params }, }); } -export function buildElasticMessage(params: Partial = {}): Message { +export function buildAssistantInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, ...{ - message: { role: MessageRole.Elastic, data: { key: 'value', nestedData: { foo: 'bar' } } }, + content: 'This is "leftpad":', + role: MessageRole.Assistant, + data: { key: 'value', nestedData: { foo: 'bar' } }, + ...params, }, - ...params, }); } -export function buildFunctionMessage(params: Partial = {}): Message { +export function buildElasticInnerMessage( + params: Partial = {} +): Message['message'] { + return cloneDeep({ + ...{ role: MessageRole.Elastic, data: { key: 'value', nestedData: { foo: 'bar' } }, ...params }, + }); +} + +export function buildFunctionInnerMessage( + params: Partial = {} +): Message['message'] { return cloneDeep({ - ...baseMessage, ...{ - message: { role: MessageRole.Function }, - function_call: { - name: 'leftpad', - args: '{ foo: "bar" }', - trigger: MessageRole.User, - }, + role: MessageRole.Function, + }, + function_call: { + name: 'leftpad', + args: '{ foo: "bar" }', + trigger: MessageRole.User, }, ...params, }); From a6527b4318b51de83b2660a9e11af718cf7e9f0f Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 25 Jul 2023 11:42:30 +0200 Subject: [PATCH 22/23] Integrate changes --- package.json | 1 - .../public/components/assistant_avatar.tsx | 5 +- .../public/components/chat/chat_item.tsx | 36 ++++++++- .../public/components/chat/chat_timeline.tsx | 1 + .../components/insight/insight.stories.tsx | 42 ---------- .../insight/insight_generated_response.tsx | 79 ------------------- .../public/hooks/use_streaming_words.ts | 28 ------- yarn.lock | 5 -- 8 files changed, 36 insertions(+), 161 deletions(-) delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx delete mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_streaming_words.ts diff --git a/package.json b/package.json index 036028bad4221..c8fa01e81009e 100644 --- a/package.json +++ b/package.json @@ -772,7 +772,6 @@ "@opentelemetry/sdk-metrics-base": "^0.31.0", "@opentelemetry/semantic-conventions": "^1.4.0", "@reduxjs/toolkit": "1.7.2", - "@sindresorhus/fnv1a": "^3.0.0", "@slack/webhook": "^5.0.4", "@tanstack/react-query": "^4.29.12", "@tanstack/react-query-devtools": "^4.29.12", diff --git a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx index 5351600713a22..422bd42f16c3f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/assistant_avatar.tsx @@ -4,10 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { ReactNode } from 'react'; export interface AssistantAvatarProps { - size: keyof typeof sizeMap; + size?: keyof typeof sizeMap; + children?: ReactNode; } export const sizeMap = { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx index b1ad9916dfc82..8b0a5ee28d594 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_item.tsx @@ -7,11 +7,14 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; import { EuiText, EuiComment } from '@elastic/eui'; import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { MessageRole, Message } from '../../../common/types'; import { ChatItemAvatar } from './chat_item_avatar'; import { ChatItemTitle } from './chat_item_title'; +import { MessagePanel } from '../message_panel/message_panel'; +import { FeedbackButtons, Feedback } from '../feedback_buttons'; const roleMap = { [MessageRole.User]: i18n.translate( @@ -45,20 +48,45 @@ export interface ChatItemProps { dateFormat: string; index: number; message: Message; + onFeedbackClick: (feedback: Feedback) => void; } -export function ChatItem({ currentUser, dateFormat, index, message }: ChatItemProps) { +export function ChatItem({ + currentUser, + dateFormat, + index, + message, + onFeedbackClick, +}: ChatItemProps) { return ( } timelineAvatar={} username={roleMap[message.message.role]} + css={ + message.message.role !== MessageRole.User + ? css` + .euiCommentEvent__body { + padding: 0; + } + ` + : '' + } > {message.message.content ? ( - -

{message.message.content}

-
+ <> + {message.message.role === MessageRole.User ? ( + +

{message.message.content}

+
+ ) : ( + } + /> + )} + ) : null}
); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx index 0576349a16b48..dc5052b41360a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_timeline.tsx @@ -30,6 +30,7 @@ export function ChatTimeline({ messages = [] }: ChatTimelineProps) { dateFormat={dateFormat} index={index} message={message} + onFeedbackClick={() => {}} /> ))} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx deleted file mode 100644 index 8ca01d2f2588c..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight.stories.tsx +++ /dev/null @@ -1,42 +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 { ComponentStory } from '@storybook/react'; - -import { Insight as Component, InsightProps } from './insight'; -import { KibanaReactStorybookDecorator } from '../../utils/storybook_decorator'; - -export default { - component: Component, - title: 'app/Molecules/Insight', - argTypes: { - debug: { - control: { - type: 'boolean', - }, - }, - }, - decorators: [KibanaReactStorybookDecorator], -}; - -const Template: ComponentStory = (props: InsightProps) => ( - -); - -const defaultProps = { - title: 'Elastic Assistant', - actions: [ - { id: 'foo', label: 'Put hands in pockets', handler: () => {} }, - { id: 'bar', label: 'Drop kick', handler: () => {} }, - ], - description: 'What is the root cause of performance degradation in my service?', - debug: true, -}; - -export const Insight = Template.bind({}); -Insight.args = defaultProps; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx b/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx deleted file mode 100644 index af09e81bde361..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/insight/insight_generated_response.tsx +++ /dev/null @@ -1,79 +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 { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiPanel, - EuiText, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { Feedback, FeedbackButtons } from '../feedback_buttons'; -import { useStreamingText } from '../../hooks/use_streaming_words'; -interface InsightGeneratedResponseProps { - answer: string; - onClickFeedback: (feedback: Feedback) => void; - onClickRegenerate: () => void; - onClickStartChat: () => void; -} - -export function InsightGeneratedResponse({ - onClickFeedback, - onClickRegenerate, - onClickStartChat, -}: InsightGeneratedResponseProps) { - const answer = useStreamingText({ - message: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -Aliquam commodo sollicitudin erat in ultrices. Vestibulum euismod ex ac lectus semper hendrerit. - -Morbi mattis odio justo, in ullamcorper metus aliquet eu. Praesent risus velit, rutrum ac magna non, vehicula vestibulum sapien. Quisque pulvinar eros eu finibus iaculis. - -Morbi dapibus sapien lacus, vitae suscipit ex egestas pharetra. In velit eros, fermentum sit amet augue ut, aliquam sodales nulla. Nunc mattis lobortis eros sit amet dapibus. - - Morbi non faucibus massa. Aliquam sed augue in eros ornare luctus sit amet cursus dolor. Pellentesque pellentesque lorem eu odio auctor convallis. Sed sodales felis at velit tempus tincidunt. Nulla sed ante cursus nibh mollis blandit. In mattis imperdiet tellus. Vestibulum nisl turpis, efficitur quis sollicitudin id, mollis in arcu. Vestibulum pulvinar tincidunt magna, vitae facilisis massa congue quis. Cras commodo efficitur tellus, et commodo risus rutrum at.`, - }); - return ( - - -

{answer}

-
- - - - - - - - - - - - - {i18n.translate('xpack.observabilityAiAssistant.insight.response.regenerate', { - defaultMessage: 'Regenerate', - })} - - - - - - {i18n.translate('xpack.observabilityAiAssistant.insight.response.startChat', { - defaultMessage: 'Start chat', - })} - - - - - -
- ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_streaming_words.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_streaming_words.ts deleted file mode 100644 index 4bd4cedd1bff0..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_streaming_words.ts +++ /dev/null @@ -1,28 +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 { useEffect, useState } from 'react'; - -interface UseStreamingTextProps { - message: string; -} - -export function useStreamingText({ message }: UseStreamingTextProps) { - const [chatMessages, setChatMessages] = useState(''); - - useEffect(() => { - const words = message.split(' '); - - for (let i = 0; i < words.length; i++) { - setTimeout(() => { - setChatMessages((prevState) => `${prevState} ${words[i]}`); - }, i * 50); // Adjust typing speed here (milliseconds per word) - } - }, [message]); - - return chatMessages; -} diff --git a/yarn.lock b/yarn.lock index 0b25249c8f4cd..5625c7ad381af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6847,11 +6847,6 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@sindresorhus/fnv1a@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/fnv1a/-/fnv1a-3.0.0.tgz#e8ce2e7c7738ec8c354867d38e3bfcde622b87ca" - integrity sha512-M6pmbdZqAryzjZ4ELAzrdCMoMZk5lH/fshKrapfSeXdf2W+GDqZvPmfXaNTZp43//FVbSwkTPwpEMnehSyskkQ== - "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" From d4d2ad315ee1c1b8d264d4eb428a0a63194ca5f5 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 25 Jul 2023 11:56:56 +0200 Subject: [PATCH 23/23] Remove background for EA --- .../public/components/message_panel/message_panel.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx index be39fa01f11df..033b951b202a4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.tsx @@ -9,7 +9,6 @@ import { EuiFlexItem, EuiHorizontalRule, EuiIcon, - EuiPanel, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -24,7 +23,7 @@ interface Props { export function MessagePanel(props: Props) { return ( - + <> {props.body} {props.error ? ( <> @@ -51,6 +50,6 @@ export function MessagePanel(props: Props) { {props.controls} ) : null} - + ); }