diff --git a/x-pack/plugins/apm/public/assistant_functions/get_apm_service_summary.ts b/x-pack/plugins/apm/public/assistant_functions/get_apm_service_summary.ts index fbbd35524d43da..189633ec959750 100644 --- a/x-pack/plugins/apm/public/assistant_functions/get_apm_service_summary.ts +++ b/x-pack/plugins/apm/public/assistant_functions/get_apm_service_summary.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; import { callApmApi } from '../services/rest/create_call_apm_api'; +import { NON_EMPTY_STRING } from '../utils/non_empty_string_ref'; export function registerGetApmServiceSummaryFunction({ registerFunction, @@ -35,20 +36,20 @@ alerts and anomalies.`, type: 'object', properties: { 'service.name': { - type: 'string', + ...NON_EMPTY_STRING, description: 'The name of the service that should be summarized.', }, 'service.environment': { - type: 'string', + ...NON_EMPTY_STRING, description: 'The environment that the service is running in', }, start: { - type: 'string', + ...NON_EMPTY_STRING, description: 'The start of the time range, in Elasticsearch date math, like `now`.', }, end: { - type: 'string', + ...NON_EMPTY_STRING, description: 'The end of the time range, in Elasticsearch date math, like `now-24h`.', }, diff --git a/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts b/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts new file mode 100644 index 00000000000000..e6b984c4c88b12 --- /dev/null +++ b/x-pack/plugins/apm/public/assistant_functions/get_apm_services_list.ts @@ -0,0 +1,55 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { RegisterFunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common/types'; +import { callApmApi } from '../services/rest/create_call_apm_api'; +import { NON_EMPTY_STRING } from '../utils/non_empty_string_ref'; + +export function registerGetApmServicesListFunction({ + registerFunction, +}: { + registerFunction: RegisterFunctionDefinition; +}) { + registerFunction( + { + name: 'get_apm_services_list', + contexts: ['apm'], + description: `Gets a list of services`, + descriptionForUser: i18n.translate( + 'xpack.apm.observabilityAiAssistant.functions.registerGetApmServicesList.descriptionForUser', + { + defaultMessage: `Gets the list of monitored services. Only returns the service names`, + } + ), + parameters: { + type: 'object', + properties: { + start: { + ...NON_EMPTY_STRING, + description: + 'The start of the time range, in Elasticsearch date math, like `now`.', + }, + end: { + ...NON_EMPTY_STRING, + description: + 'The end of the time range, in Elasticsearch date math, like `now-24h`.', + }, + }, + required: ['start', 'end'], + } as const, + }, + async ({ arguments: args }, signal) => { + return callApmApi('GET /internal/apm/assistant/get_services_list', { + signal, + params: { + query: args, + }, + }); + } + ); +} diff --git a/x-pack/plugins/apm/public/assistant_functions/index.ts b/x-pack/plugins/apm/public/assistant_functions/index.ts index f83e9fa6fc575e..693a10ec37b48f 100644 --- a/x-pack/plugins/apm/public/assistant_functions/index.ts +++ b/x-pack/plugins/apm/public/assistant_functions/index.ts @@ -18,6 +18,7 @@ import { import { registerGetApmCorrelationsFunction } from './get_apm_correlations'; import { registerGetApmDownstreamDependenciesFunction } from './get_apm_downstream_dependencies'; import { registerGetApmErrorDocumentFunction } from './get_apm_error_document'; +import { registerGetApmServicesListFunction } from './get_apm_services_list'; import { registerGetApmServiceSummaryFunction } from './get_apm_service_summary'; import { registerGetApmTimeseriesFunction } from './get_apm_timeseries'; @@ -64,6 +65,10 @@ export async function registerAssistantFunctions({ registerFunction, }); + registerGetApmServicesListFunction({ + registerFunction, + }); + registerContext({ name: 'apm', description: ` diff --git a/x-pack/plugins/apm/public/utils/non_empty_string_ref.ts b/x-pack/plugins/apm/public/utils/non_empty_string_ref.ts new file mode 100644 index 00000000000000..94cac08fa21982 --- /dev/null +++ b/x-pack/plugins/apm/public/utils/non_empty_string_ref.ts @@ -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. + */ + +export const NON_EMPTY_STRING = { + type: 'string' as const, + minLength: 1, +}; diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_services_list/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_services_list/index.ts new file mode 100644 index 00000000000000..1e9fc362155395 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_services_list/index.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 * as t from 'io-ts'; +import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { rangeQuery } from '@kbn/observability-plugin/server'; +import datemath from '@elastic/datemath'; +import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { SERVICE_NAME } from '../../../../common/es_fields/apm'; + +export const getApmServicesListRouteRt = t.type({ + start: t.string, + end: t.string, +}); + +export interface ApmServicesListContent { + hasMore: boolean; + services: Array<{ [SERVICE_NAME]: string }>; +} + +export async function getApmServicesList({ + arguments: args, + apmEventClient, +}: { + arguments: t.TypeOf; + apmEventClient: APMEventClient; +}): Promise { + const start = datemath.parse(args.start)!.valueOf(); + const end = datemath.parse(args.end)!.valueOf(); + + const terms = await apmEventClient.termsEnum('get_services_list', { + apm: { + events: [ + ProcessorEvent.transaction, + ProcessorEvent.span, + ProcessorEvent.metric, + ProcessorEvent.error, + ], + }, + index_filter: { + bool: { + filter: rangeQuery(start, end), + }, + }, + size: 20, + field: SERVICE_NAME, + }); + + return { + services: terms.terms.map((term) => ({ [SERVICE_NAME]: term })), + hasMore: !terms.complete, + }; +} diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts index bd2d32c90907ed..1b749548410024 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/route.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/route.ts @@ -33,6 +33,11 @@ import { getApmTimeseries, getApmTimeseriesRt, } from './get_apm_timeseries'; +import { + ApmServicesListContent, + getApmServicesList, + getApmServicesListRouteRt, +} from './get_services_list'; const getApmTimeSeriesRoute = createApmServerRoute({ endpoint: 'POST /internal/apm/assistant/get_apm_timeseries', @@ -184,10 +189,33 @@ const getApmErrorDocRoute = createApmServerRoute({ }, }); +const getApmServicesListRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/assistant/get_services_list', + params: t.type({ + query: getApmServicesListRouteRt, + }), + options: { + tags: ['access:apm'], + }, + handler: async (resources): Promise<{ content: ApmServicesListContent }> => { + const { params } = resources; + const apmEventClient = await getApmEventClient(resources); + const { query } = params; + + return { + content: await getApmServicesList({ + apmEventClient, + arguments: query, + }), + }; + }, +}); + export const assistantRouteRepository = { ...getApmTimeSeriesRoute, ...getApmServiceSummaryRoute, ...getApmErrorDocRoute, ...getApmCorrelationValuesRoute, ...getDownstreamDependenciesRoute, + ...getApmServicesListRoute, }; diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index d1d9e8ef313321..1d8be57fd53b37 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -64,6 +64,7 @@ export interface KnowledgeBaseEntry { confidence: 'low' | 'medium' | 'high'; is_correction: boolean; public: boolean; + labels: Record; } export type CompatibleJSONSchema = Exclude; diff --git a/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/calculate_auto.js b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/calculate_auto.js new file mode 100644 index 00000000000000..bd4d6e51ccc0dd --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/calculate_auto.js @@ -0,0 +1,78 @@ +/* + * 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'; +const d = moment.duration; + +const roundingRules = [ + [d(500, 'ms'), d(100, 'ms')], + [d(5, 'second'), d(1, 'second')], + [d(7.5, 'second'), d(5, 'second')], + [d(15, 'second'), d(10, 'second')], + [d(45, 'second'), d(30, 'second')], + [d(3, 'minute'), d(1, 'minute')], + [d(9, 'minute'), d(5, 'minute')], + [d(20, 'minute'), d(10, 'minute')], + [d(45, 'minute'), d(30, 'minute')], + [d(2, 'hour'), d(1, 'hour')], + [d(6, 'hour'), d(3, 'hour')], + [d(24, 'hour'), d(12, 'hour')], + [d(1, 'week'), d(1, 'd')], + [d(3, 'week'), d(1, 'week')], + [d(1, 'year'), d(1, 'month')], + [Infinity, d(1, 'year')], +]; + +const revRoundingRules = roundingRules.slice(0).reverse(); + +function find(rules, check, last) { + function pick(buckets, duration) { + const target = duration / buckets; + let lastResp = null; + + for (let i = 0; i < rules.length; i++) { + const rule = rules[i]; + const resp = check(rule[0], rule[1], target); + + if (resp == null) { + if (!last) continue; + if (lastResp) return lastResp; + break; + } + + if (!last) return resp; + lastResp = resp; + } + + // fallback to just a number of milliseconds, ensure ms is >= 1 + const ms = Math.max(Math.floor(target), 1); + return moment.duration(ms, 'ms'); + } + + return (buckets, duration) => { + const interval = pick(buckets, duration); + if (interval) return moment.duration(interval._data); + }; +} + +export const calculateAuto = { + near: find( + revRoundingRules, + function near(bound, interval, target) { + if (bound > target) return interval; + }, + true + ), + + lessThan: find(revRoundingRules, function lessThan(_bound, interval, target) { + if (interval < target) return interval; + }), + + atLeast: find(revRoundingRules, function atLeast(_bound, interval, target) { + if (interval <= target) return interval; + }), +}; diff --git a/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/index.test.ts b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/index.test.ts new file mode 100644 index 00000000000000..e91b6b44dee7fa --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/index.test.ts @@ -0,0 +1,72 @@ +/* + * 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 { getBucketSize } from '.'; +import moment from 'moment'; + +describe('getBuckets', () => { + describe("minInterval 'auto'", () => { + it('last 15 minutes', () => { + const start = moment().subtract(15, 'minutes').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 10, + intervalString: '10s', + }); + }); + it('last 1 hour', () => { + const start = moment().subtract(1, 'hour').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 30, + intervalString: '30s', + }); + }); + it('last 1 week', () => { + const start = moment().subtract(1, 'week').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 3600, + intervalString: '3600s', + }); + }); + it('last 30 days', () => { + const start = moment().subtract(30, 'days').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 43200, + intervalString: '43200s', + }); + }); + it('last 1 year', () => { + const start = moment().subtract(1, 'year').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: 'auto' })).toEqual({ + bucketSize: 86400, + intervalString: '86400s', + }); + }); + }); + describe("minInterval '30s'", () => { + it('last 15 minutes', () => { + const start = moment().subtract(15, 'minutes').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({ + bucketSize: 30, + intervalString: '30s', + }); + }); + it('last 1 year', () => { + const start = moment().subtract(1, 'year').valueOf(); + const end = moment.now(); + expect(getBucketSize({ start, end, minInterval: '30s' })).toEqual({ + bucketSize: 86400, + intervalString: '86400s', + }); + }); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/index.ts b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/index.ts new file mode 100644 index 00000000000000..ca1afaf41c1a61 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/index.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 moment from 'moment'; +// @ts-ignore +import { calculateAuto } from './calculate_auto'; +import { unitToSeconds } from './unit_to_seconds'; + +export function getBucketSize({ + start, + end, + minInterval, + buckets = 100, +}: { + start: number; + end: number; + minInterval: string; + buckets?: number; +}) { + const duration = moment.duration(end - start, 'ms'); + const bucketSize = Math.max(calculateAuto.near(buckets, duration)?.asSeconds() ?? 0, 1); + const intervalString = `${bucketSize}s`; + const matches = minInterval && minInterval.match(/^([\d]+)([shmdwMy]|ms)$/); + const minBucketSize = matches ? Number(matches[1]) * unitToSeconds(matches[2]) : 0; + + if (bucketSize < minBucketSize) { + return { + bucketSize: minBucketSize, + intervalString: minInterval, + }; + } + + return { bucketSize, intervalString }; +} diff --git a/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/unit_to_seconds.ts b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/unit_to_seconds.ts new file mode 100644 index 00000000000000..eec81dd3fcd294 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/utils/get_bucket_size/unit_to_seconds.ts @@ -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 moment, { unitOfTime as UnitOfTIme } from 'moment'; + +function getDurationAsSeconds(value: number, unitOfTime: UnitOfTIme.Base) { + return moment.duration(value, unitOfTime).asSeconds(); +} + +const units = { + ms: getDurationAsSeconds(1, 'millisecond'), + s: getDurationAsSeconds(1, 'second'), + m: getDurationAsSeconds(1, 'minute'), + h: getDurationAsSeconds(1, 'hour'), + d: getDurationAsSeconds(1, 'day'), + w: getDurationAsSeconds(1, 'week'), + M: getDurationAsSeconds(1, 'month'), + y: getDurationAsSeconds(1, 'year'), +}; + +export function unitToSeconds(unit: string) { + return units[unit as keyof typeof units]; +} diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index a1d535a4c14c82..e0299246377a38 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -7,7 +7,7 @@ "server": true, "browser": true, "configPath": ["xpack", "observabilityAIAssistant"], - "requiredPlugins": ["triggersActionsUi", "actions", "security", "features", "observabilityShared", "taskManager", "lens", "dataViews"], + "requiredPlugins": ["triggersActionsUi", "actions", "security", "features", "observabilityShared", "taskManager", "lens", "dataViews", "ruleRegistry"], "requiredBundles": ["kibanaReact", "kibanaUtils", "fieldFormats"], "optionalPlugins": [], "extraPublicDirs": [] diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx index 2ab602e429ce17..1f817860863709 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_panel.stories.tsx @@ -6,6 +6,7 @@ */ import { EuiPanel } from '@elastic/eui'; import { ComponentMeta, ComponentStoryObj } from '@storybook/react'; +import dedent from 'dedent'; import React from 'react'; import { FeedbackButtons } from '../feedback_buttons'; import { MessagePanel as Component } from './message_panel'; @@ -61,6 +62,32 @@ export const ContentFailed: ComponentStoryObj = { }, }; +export const ContentTable: ComponentStoryObj = { + args: { + body: ( + + ), + }, +}; + export const Controls: ComponentStoryObj = { args: { body: , diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx index 7aeae624a02538..6fd9a1c70923de 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/message_text.tsx @@ -4,13 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiText } from '@elastic/eui'; +import { + EuiMarkdownFormat, + EuiSpacer, + EuiText, + getDefaultEuiMarkdownParsingPlugins, + getDefaultEuiMarkdownProcessingPlugins, +} from '@elastic/eui'; import { css } from '@emotion/css'; -import { euiThemeVars } from '@kbn/ui-theme'; import classNames from 'classnames'; import type { Code, InlineCode, Parent, Text } from 'mdast'; -import React from 'react'; -import ReactMarkdown from 'react-markdown'; +import React, { useMemo } from 'react'; import type { Node } from 'unist'; import { v4 } from 'uuid'; @@ -44,7 +48,7 @@ const cursorCss = css` const Cursor = () => ; -const CURSOR = `{{${v4()}}`; +const CURSOR = `{{${v4()}}}`; const loadingCursorPlugin = () => { const visitor = (node: Node, parent?: Parent) => { @@ -72,9 +76,6 @@ const loadingCursorPlugin = () => { parent!.children.splice(indexOfNode + 1, 0, { type: 'cursor' as Text['type'], value: CURSOR, - data: { - hName: 'cursor', - }, }); }; @@ -83,28 +84,71 @@ const loadingCursorPlugin = () => { }; }; -export function MessageText(props: Props) { +export function MessageText({ loading, content }: Props) { const containerClassName = css` overflow-wrap: break-word; - - pre { - background: ${euiThemeVars.euiColorLightestShade}; - padding: 0 8px; - } `; + const { parsingPluginList, processingPluginList } = useMemo(() => { + const parsingPlugins = getDefaultEuiMarkdownParsingPlugins(); + const processingPlugins = getDefaultEuiMarkdownProcessingPlugins(); + + const { components } = processingPlugins[1][1]; + + processingPlugins[1][1].components = { + ...components, + cursor: Cursor, + table: (props) => ( + <> +
+ {' '} + + + + + ), + th: (props) => { + const { children, ...rest } = props; + return ( + + ); + }, + tr: (props) => , + td: (props) => { + const { children, ...rest } = props; + return ( + + ); + }, + }; + + return { + parsingPluginList: [loadingCursorPlugin, ...parsingPlugins], + processingPluginList: processingPlugins, + }; + }, []); + return ( - - } + - {`${props.content}${props.loading ? CURSOR : ''}`} - + {`${content}${loading ? CURSOR : ''}`} + ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts b/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts new file mode 100644 index 00000000000000..03ea9a055fcc7f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/functions/alerts.ts @@ -0,0 +1,80 @@ +/* + * 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 { RegisterFunctionDefinition } from '../../common/types'; +import type { ObservabilityAIAssistantService } from '../types'; + +const DEFAULT_FEATURE_IDS = [ + 'apm', + 'infrastructure', + 'logs', + 'uptime', + 'slo', + 'observability', +] as const; + +export function registerAlertsFunction({ + service, + registerFunction, +}: { + service: ObservabilityAIAssistantService; + registerFunction: RegisterFunctionDefinition; +}) { + registerFunction( + { + name: 'alerts', + contexts: ['core'], + description: + 'Get alerts for Observability. Display the response in tabular format if appropriate.', + descriptionForUser: 'Get alerts for Observability', + parameters: { + type: 'object', + additionalProperties: false, + properties: { + featureIds: { + type: 'array', + additionalItems: false, + items: { + type: 'string', + enum: DEFAULT_FEATURE_IDS, + }, + description: + 'The Observability apps for which to retrieve alerts. By default it will return alerts for all apps.', + }, + start: { + type: 'string', + description: 'The start of the time range, in Elasticsearch date math, like `now`.', + }, + end: { + type: 'string', + description: 'The end of the time range, in Elasticsearch date math, like `now-24h`.', + }, + filter: { + type: 'string', + description: + 'a KQL query to filter the data by. If no filter should be applied, leave it empty.', + }, + }, + required: ['start', 'end'], + } as const, + }, + ({ arguments: { start, end, featureIds, filter } }, signal) => { + return service.callApi('POST /internal/observability_ai_assistant/functions/alerts', { + params: { + body: { + start, + end, + featureIds: + featureIds && featureIds.length > 0 ? featureIds : DEFAULT_FEATURE_IDS.concat(), + filter, + }, + }, + signal, + }); + } + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts b/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts index 214b157fe2358d..546bd2bea45749 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/elasticsearch.ts @@ -21,7 +21,7 @@ export function registerElasticsearchFunction({ name: 'elasticsearch', contexts: ['core'], description: - 'Call Elasticsearch APIs on behalf of the user. Make sure the request body is valid for the API that you are using.', + 'Call Elasticsearch APIs on behalf of the user. Make sure the request body is valid for the API that you are using. Only call this function when the user has explicitly requested it.', descriptionForUser: 'Call Elasticsearch APIs on behalf of the user', parameters: { type: 'object', @@ -35,6 +35,10 @@ export function registerElasticsearchFunction({ type: 'string', description: 'The path of the Elasticsearch endpoint, including query parameters', }, + body: { + type: 'object', + description: 'The body of the request', + }, }, required: ['method', 'path'] as const, }, diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts index a077c24748997a..47aaba888d26c0 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts @@ -6,25 +6,30 @@ */ import dedent from 'dedent'; +import type { CoreStart } from '@kbn/core/public'; import type { RegisterContextDefinition, RegisterFunctionDefinition } from '../../common/types'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../types'; import type { ObservabilityAIAssistantService } from '../types'; import { registerElasticsearchFunction } from './elasticsearch'; +import { registerKibanaFunction } from './kibana'; import { registerLensFunction } from './lens'; import { registerRecallFunction } from './recall'; import { registerSummarisationFunction } from './summarise'; +import { registerAlertsFunction } from './alerts'; export async function registerFunctions({ registerFunction, registerContext, service, pluginsStart, + coreStart, signal, }: { registerFunction: RegisterFunctionDefinition; registerContext: RegisterContextDefinition; service: ObservabilityAIAssistantService; pluginsStart: ObservabilityAIAssistantPluginStartDependencies; + coreStart: CoreStart; signal: AbortSignal; }) { return service @@ -34,7 +39,7 @@ export async function registerFunctions({ .then((response) => { const isReady = response.ready; - let description = `You have the ability to call Elasticsearch APIs with the "elasticsearch" function or create visualisations using Lens with the "lens" function in the context of this chat.`; + let description = `You have the ability to call Elasticsearch APIs with the "elasticsearch" function or create visualisations using Lens with the "lens" function in the context of this chat. Use the Elasticsearch function only when requested by the user.`; if (isReady) { description += `You can use the "summarise" functions to store new information you have learned in a knowledge database. Once you have established that you did not know the answer to a question, and the user gave you this information, it's important that you create a summarisation of what you have learned and store it in the knowledge database. When you create this summarisation, make sure you craft it in a way that can be recalled with a semantic search later. @@ -70,6 +75,8 @@ export async function registerFunctions({ } registerElasticsearchFunction({ service, registerFunction }); + registerKibanaFunction({ service, registerFunction, coreStart }); + registerAlertsFunction({ service, registerFunction }); registerContext({ name: 'core', diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/kibana.ts b/x-pack/plugins/observability_ai_assistant/public/functions/kibana.ts new file mode 100644 index 00000000000000..5ad877b2c2bff2 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/functions/kibana.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 type { CoreStart } from '@kbn/core/public'; +import type { RegisterFunctionDefinition } from '../../common/types'; +import type { ObservabilityAIAssistantService } from '../types'; + +export function registerKibanaFunction({ + service, + registerFunction, + coreStart, +}: { + service: ObservabilityAIAssistantService; + registerFunction: RegisterFunctionDefinition; + coreStart: CoreStart; +}) { + registerFunction( + { + name: 'kibana', + contexts: ['core'], + description: + 'Call Kibana APIs on behalf of the user. Only call this function when the user has explicitly requested it, and you know how to call it, for example by querying the knowledge base or having the user explain it to you. Assume that pathnames, bodies and query parameters may have changed since your knowledge cut off date.', + descriptionForUser: 'Call Kibana APIs on behalf of the user', + parameters: { + type: 'object', + additionalProperties: false, + properties: { + method: { + type: 'string', + description: 'The HTTP method of the Kibana endpoint', + enum: ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] as const, + }, + pathname: { + type: 'string', + description: 'The pathname of the Kibana endpoint, excluding query parameters', + }, + query: { + type: 'object', + description: 'The query parameters, as an object', + additionalProperties: { + type: 'string', + }, + }, + body: { + type: 'object', + description: 'The body of the request', + }, + }, + required: ['method', 'pathname', 'body'] as const, + }, + }, + ({ arguments: { method, pathname, body, query } }, signal) => { + return coreStart.http + .fetch(pathname, { + method, + body: body ? JSON.stringify(body) : undefined, + query, + signal, + }) + .then((response) => { + return { content: response }; + }); + } + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx b/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx index b25c3a3717c609..9c283f290b8df3 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/functions/lens.tsx @@ -106,11 +106,13 @@ export function registerLensFunction({ 'Use this function to create custom visualisations, using Lens, that can be saved to dashboards.', parameters: { type: 'object', + additionalProperties: false, properties: { layers: { type: 'array', items: { type: 'object', + additionalProperties: false, properties: { label: { type: 'string', @@ -126,6 +128,7 @@ export function registerLensFunction({ }, format: { type: 'object', + additionalProperties: false, properties: { id: { type: 'string', @@ -149,6 +152,7 @@ export function registerLensFunction({ }, breakdown: { type: 'object', + additionalProperties: false, properties: { field: { type: 'string', diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts index a571287acf8b24..cefb0a713e2a53 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/recall.ts @@ -25,6 +25,7 @@ export function registerRecallFunction({ descriptionForUser: 'This function allows the assistant to recall previous learnings.', parameters: { type: 'object', + additionalProperties: false, properties: { query: { type: 'string', diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts b/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts index 3fe55385a74ff3..010cdb608fde04 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/summarise.ts @@ -25,6 +25,7 @@ export function registerSummarisationFunction({ 'This function allows the Elastic Assistant to summarise things from the conversation.', parameters: { type: 'object', + additionalProperties: false, properties: { id: { type: 'string', @@ -73,6 +74,7 @@ export function registerSummarisationFunction({ is_correction: isCorrection, confidence, public: isPublic, + labels: {}, }, }, signal, diff --git a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx index 88853a5ce1bcd6..965a97e4a0a870 100644 --- a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx @@ -107,6 +107,7 @@ export class ObservabilityAIAssistantPlugin service, signal, pluginsStart, + coreStart, registerContext, registerFunction, }); diff --git a/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.ts b/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.ts index b1e25233e9c3b3..da3f5f9947b0f7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/get_assistant_setup_message.ts @@ -16,7 +16,13 @@ export function getAssistantSetupMessage({ contexts }: { contexts: ContextDefini role: MessageRole.System as const, content: [ dedent( - `You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems. You can help them visualise and analyze data, investigate their systems, perform root cause analysis or identify optimisation opportunities.` + `You are a helpful assistant for Elastic Observability. Your goal is to help the Elastic Observability users to quickly assess what is happening in their observed systems. You can help them visualise and analyze data, investigate their systems, perform root cause analysis or identify optimisation opportunities. + + It's very important to not assume what the user is meaning. Ask them for clarification if needed. + + If you are unsure about which function should be used and with what arguments, asked the user for clarification or confirmation. + + You can use (Github-flavored) Markdown in your responses. Use it to nicely format output, like a table.` ), ] .concat(contexts.map((context) => context.description)) diff --git a/x-pack/plugins/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_ai_assistant/server/plugin.ts index b13f22fd2fda00..01bd4fafb71c70 100644 --- a/x-pack/plugins/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_ai_assistant/server/plugin.ts @@ -31,6 +31,7 @@ import { ObservabilityAIAssistantPluginSetupDependencies, ObservabilityAIAssistantPluginStartDependencies, } from './types'; +import { addLensDocsToKb } from './service/kb_service/kb_docs/lens'; export class ObservabilityAIAssistantPlugin implements @@ -114,7 +115,7 @@ export class ObservabilityAIAssistantPlugin taskManager: plugins.taskManager, }); - // addLensDocsToKb(service); + addLensDocsToKb(service); registerServerRoutes({ core, diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts index 954262c5a03bee..c88070d00a137b 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts @@ -4,11 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as t from 'io-ts'; -import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils'; +import datemath from '@elastic/datemath'; import { notImplemented } from '@hapi/boom'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils'; +import * as t from 'io-ts'; +import { omit } from 'lodash'; +import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common'; +import type { KnowledgeBaseEntry } from '../../../common/types'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; -import { KnowledgeBaseEntry } from '../../../common/types'; const functionElasticsearchRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/functions/elasticsearch', @@ -47,6 +51,94 @@ const functionElasticsearchRoute = createObservabilityAIAssistantServerRoute({ }, }); +const OMITTED_ALERT_FIELDS = [ + 'tags', + 'event.action', + 'event.kind', + 'kibana.alert.rule.execution.uuid', + 'kibana.alert.rule.revision', + 'kibana.alert.rule.tags', + 'kibana.alert.rule.uuid', + 'kibana.alert.workflow_status', + 'kibana.space_ids', + 'kibana.alert.time_range', + 'kibana.version', +] as const; + +const functionAlertsRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/functions/alerts', + options: { + tags: ['access:ai_assistant'], + }, + params: t.type({ + body: t.intersection([ + t.type({ + featureIds: t.array(t.string), + start: t.string, + end: t.string, + }), + t.partial({ + filter: t.string, + }), + ]), + }), + handler: async ( + resources + ): Promise<{ + content: { + total: number; + alerts: ParsedTechnicalFields[]; + }; + }> => { + const { + featureIds, + start: startAsDatemath, + end: endAsDatemath, + filter, + } = resources.params.body; + + const racContext = await resources.context.rac; + const alertsClient = await racContext.getAlertsClient(); + + const start = datemath.parse(startAsDatemath)!.valueOf(); + const end = datemath.parse(endAsDatemath)!.valueOf(); + + const kqlQuery = !filter ? [] : [toElasticsearchQuery(fromKueryExpression(filter))]; + + const response = await alertsClient.find({ + featureIds, + + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: start, + lte: end, + }, + }, + }, + ...kqlQuery, + ], + }, + }, + }); + + // trim some fields + const alerts = response.hits.hits.map((hit) => + omit(hit._source, ...OMITTED_ALERT_FIELDS) + ) as unknown as ParsedTechnicalFields[]; + + return { + content: { + total: (response.hits as { total: { value: number } }).total.value, + alerts, + }, + }; + }, +}); + const functionRecallRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/functions/recall', params: t.type({ @@ -57,7 +149,9 @@ const functionRecallRoute = createObservabilityAIAssistantServerRoute({ options: { tags: ['access:ai_assistant'], }, - handler: async (resources): Promise<{ entries: KnowledgeBaseEntry[] }> => { + handler: async ( + resources + ): Promise<{ entries: Array> }> => { const client = await resources.service.getClient({ request: resources.request }); if (!client) { @@ -77,6 +171,7 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ confidence: t.union([t.literal('low'), t.literal('medium'), t.literal('high')]), is_correction: toBooleanRt, public: toBooleanRt, + labels: t.record(t.string, t.string), }), }), options: { @@ -95,6 +190,7 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ is_correction: isCorrection, text, public: isPublic, + labels, } = resources.params.body; return client.summarise({ @@ -104,6 +200,7 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({ is_correction: isCorrection, text, public: isPublic, + labels, }, }); }, @@ -159,4 +256,5 @@ export const functionRoutes = { ...functionSummariseRoute, ...setupKnowledgeBaseRoute, ...getKnowledgeBaseStatus, + ...functionAlertsRoute, }; diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/types.ts index e1c256aefcb0e2..1766f5c2d55428 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/types.ts @@ -5,17 +5,22 @@ * 2.0. */ +import type { CustomRequestHandlerContext, KibanaRequest } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import type { KibanaRequest, RequestHandlerContext } from '@kbn/core/server'; +import type { RacApiRequestHandlerContext } from '@kbn/rule-registry-plugin/server'; +import type { ObservabilityAIAssistantService } from '../service'; import type { ObservabilityAIAssistantPluginSetupDependencies, ObservabilityAIAssistantPluginStartDependencies, } from '../types'; -import type { ObservabilityAIAssistantService } from '../service'; + +export type ObservabilityAIAssistantRequestHandlerContext = CustomRequestHandlerContext<{ + rac: RacApiRequestHandlerContext; +}>; export interface ObservabilityAIAssistantRouteHandlerResources { request: KibanaRequest; - context: RequestHandlerContext; + context: ObservabilityAIAssistantRequestHandlerContext; logger: Logger; service: ObservabilityAIAssistantService; plugins: { 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 index ceb04475e8563b..3e574ca9682f85 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -312,7 +312,9 @@ export class ObservabilityAIAssistantClient { return createdConversation; }; - recall = async (query: string): Promise<{ entries: KnowledgeBaseEntry[] }> => { + recall = async ( + query: string + ): Promise<{ entries: Array> }> => { return this.dependencies.knowledgeBaseService.recall({ namespace: this.dependencies.namespace, user: this.dependencies.user, diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts index 2ce0868aefd307..e3a0eb9f154695 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -13,13 +13,13 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { once } from 'lodash'; -import { KnowledgeBaseEntry } from '../../common/types'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../types'; import { ObservabilityAIAssistantClient } from './client'; import { conversationComponentTemplate } from './conversation_component_template'; import { kbComponentTemplate } from './kb_component_template'; -import { KnowledgeBaseService } from './kb_service'; +import { KnowledgeBaseEntryOperationType, KnowledgeBaseService } from './kb_service'; import type { ObservabilityAIAssistantResourceNames } from './types'; +import { splitKbText } from './util/split_kb_text'; function getResourceName(resource: string) { return `.kibana-observability-ai-assistant-${resource}`; @@ -82,7 +82,7 @@ export class ObservabilityAIAssistantService { return { run: async () => { if (this.kbService) { - // await this.kbService.processQueue(); + await this.kbService.processQueue(); } }, }; @@ -256,20 +256,52 @@ export class ObservabilityAIAssistantService { }); } - async addToKnowledgeBase( + addToKnowledgeBase( entries: Array< - Omit + | { + id: string; + text: string; + } + | { + id: string; + texts: string[]; + } > - ): Promise { - await this.init(); - this.kbService!.store( - entries.map((entry) => ({ - ...entry, - '@timestamp': new Date().toISOString(), - public: true, - confidence: 'high', - is_correction: false, - })) - ); + ): void { + this.init() + .then(() => { + this.kbService!.queue( + entries.flatMap((entry) => { + const entryWithSystemProperties = { + ...entry, + '@timestamp': new Date().toISOString(), + public: true, + confidence: 'high' as const, + is_correction: false, + labels: { + document_id: entry.id, + }, + }; + + const operations = + 'texts' in entryWithSystemProperties + ? splitKbText(entryWithSystemProperties) + : [ + { + type: KnowledgeBaseEntryOperationType.Index, + document: entryWithSystemProperties, + }, + ]; + + return operations; + }) + ); + }) + .catch((error) => { + this.logger.error( + `Could not index ${entries.length} entries because of an initialisation error` + ); + this.logger.error(error); + }); } } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/kb_component_template.ts b/x-pack/plugins/observability_ai_assistant/server/service/kb_component_template.ts index 55d6bbd15519ce..8d0e5ff423b2ca 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/kb_component_template.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/kb_component_template.ts @@ -20,6 +20,11 @@ const date = { type: 'date' as const, }; +const dynamic = { + type: 'object' as const, + dynamic: true, +}; + export const kbComponentTemplate: ClusterComponentTemplate['component_template']['template'] = { mappings: { dynamic: false, @@ -32,6 +37,7 @@ export const kbComponentTemplate: ClusterComponentTemplate['component_template'] name: keyword, }, }, + labels: dynamic, conversation: { properties: { id: keyword, diff --git a/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts index deed5c41fd4803..d680b121cb16cb 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/kb_service/index.ts @@ -9,9 +9,10 @@ import type { QueryDslTextExpansionQuery } from '@elastic/elasticsearch/lib/api/ import { serverUnavailable } from '@hapi/boom'; import type { ElasticsearchClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; -import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; import pLimit from 'p-limit'; import pRetry from 'p-retry'; +import { map } from 'lodash'; import { INDEX_QUEUED_DOCUMENTS_TASK_ID, INDEX_QUEUED_DOCUMENTS_TASK_TYPE } from '..'; import type { KnowledgeBaseEntry } from '../../../common/types'; import type { ObservabilityAIAssistantResourceNames } from '../types'; @@ -30,10 +31,30 @@ function throwKnowledgeBaseNotReady(body: any) { throw serverUnavailable(`Knowledge base is not ready yet`, body); } +export enum KnowledgeBaseEntryOperationType { + Index = 'index', + Delete = 'delete', +} + +interface KnowledgeBaseDeleteOperation { + type: KnowledgeBaseEntryOperationType.Delete; + id?: string; + labels?: Record; +} + +interface KnowledgeBaseIndexOperation { + type: KnowledgeBaseEntryOperationType.Index; + document: KnowledgeBaseEntry; +} + +export type KnowledgeBaseEntryOperation = + | KnowledgeBaseDeleteOperation + | KnowledgeBaseIndexOperation; + export class KnowledgeBaseService { private hasSetup: boolean = false; - private entryQueue: KnowledgeBaseEntry[] = []; + private _queue: KnowledgeBaseEntryOperation[] = []; constructor(private readonly dependencies: Dependencies) { this.ensureTaskScheduled(); @@ -51,65 +72,92 @@ export class KnowledgeBaseService { }, }) .then(() => { - this.dependencies.logger.debug('Scheduled document queue task'); + this.dependencies.logger.debug('Scheduled queue task'); return this.dependencies.taskManagerStart.runSoon(INDEX_QUEUED_DOCUMENTS_TASK_ID); }) .then(() => { - this.dependencies.logger.debug('Document queue task ran'); + this.dependencies.logger.debug('Queue task ran'); }) .catch((err) => { - this.dependencies.logger.error(`Failed to schedule document queue task`); + this.dependencies.logger.error(`Failed to schedule queue task`); this.dependencies.logger.error(err); }); } + private async processOperation(operation: KnowledgeBaseEntryOperation) { + if (operation.type === KnowledgeBaseEntryOperationType.Delete) { + await this.dependencies.esClient.deleteByQuery({ + index: this.dependencies.resources.aliases.kb, + query: { + bool: { + filter: [ + ...(operation.id ? [{ term: { _id: operation.id } }] : []), + ...(operation.labels + ? map(operation.labels, (value, key) => { + return { term: { [key]: value } }; + }) + : []), + ], + }, + }, + }); + return; + } + + await this.summarise({ + entry: operation.document, + }); + } + async processQueue() { - if (!this.entryQueue.length) { + if (!this._queue.length) { return; } if (!(await this.status()).ready) { - this.dependencies.logger.debug(`Bailing on document queue task: KB is not ready yet`); + this.dependencies.logger.debug(`Bailing on queue task: KB is not ready yet`); return; } - this.dependencies.logger.debug(`Processing document queue`); + this.dependencies.logger.debug(`Processing queue`); this.hasSetup = true; - this.dependencies.logger.info(`Indexing ${this.entryQueue.length} queued entries into KB`); + this.dependencies.logger.info(`Processing ${this._queue.length} queue operations`); const limiter = pLimit(5); - const entries = this.entryQueue.concat(); + const operations = this._queue.concat(); await Promise.all( - entries.map((entry) => - limiter(() => { - this.entryQueue.splice(entries.indexOf(entry), 1); - return this.summarise({ entry }); + operations.map((operation) => + limiter(async () => { + this._queue.splice(operations.indexOf(operation), 1); + await this.processOperation(operation); }) ) ); - this.dependencies.logger.info('Indexed all queued entries into KB'); + this.dependencies.logger.info('Processed all queued operations'); } - async store(entries: KnowledgeBaseEntry[]) { - if (!entries.length) { + queue(operations: KnowledgeBaseEntryOperation[]): void { + if (!operations.length) { return; } if (!this.hasSetup) { - this.entryQueue.push(...entries); + this._queue.push(...operations); return; } const limiter = pLimit(5); - const limitedFunctions = entries.map((entry) => limiter(() => this.summarise({ entry }))); + const limitedFunctions = this._queue.map((operation) => + limiter(() => this.processOperation(operation)) + ); Promise.all(limitedFunctions).catch((err) => { - this.dependencies.logger.error(`Failed to index all knowledge base entries`); + this.dependencies.logger.error(`Failed to process all queued operations`); this.dependencies.logger.error(err); }); } @@ -122,9 +170,11 @@ export class KnowledgeBaseService { query: string; user: { name: string }; namespace: string; - }): Promise<{ entries: KnowledgeBaseEntry[] }> => { + }): Promise<{ entries: Array> }> => { try { - const response = await this.dependencies.esClient.search({ + const response = await this.dependencies.esClient.search< + Pick + >({ index: this.dependencies.resources.aliases.kb, query: { bool: { @@ -185,6 +235,7 @@ export class KnowledgeBaseService { namespace, }, pipeline: this.dependencies.resources.pipelines.kb, + refresh: false, }); } catch (error) { if (error instanceof errors.ResponseError && error.body.error.type === 'status_exception') { @@ -261,8 +312,13 @@ export class KnowledgeBaseService { wait_for: 'fully_allocated', }); } catch (error) { + this.dependencies.logger.debug('Error starting model deployment'); + this.dependencies.logger.debug(error); if ( - !(error instanceof errors.ResponseError && error.body.error.type === 'status_exception') + !( + (error instanceof errors.ResponseError && error.body.error.type === 'status_exception') || + error.body.error.type === 'resource_not_found_exception' + ) ) { throw error; } @@ -282,10 +338,11 @@ export class KnowledgeBaseService { } this.dependencies.logger.debug('Model is not allocated yet'); + this.dependencies.logger.debug(JSON.stringify(response)); return Promise.reject(new Error('Not Ready')); }, - { factor: 1, minTimeout: 10000, maxRetryTime: 20 * 60 * 1000 } + { factor: 1, minTimeout: 10000, retries: 12 } ); this.dependencies.logger.info('Model is ready'); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/kb_service/kb_docs/lens.ts b/x-pack/plugins/observability_ai_assistant/server/service/kb_service/kb_docs/lens.ts index aa0c6e3ffb9373..e4fdc8969c0104 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/kb_service/kb_docs/lens.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/kb_service/kb_docs/lens.ts @@ -12,9 +12,8 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { service.addToKnowledgeBase([ { id: 'lens_formulas_how_it_works', - text: dedent(`## How it works - - Lens formulas let you do math using a combination of Elasticsearch aggregations and + texts: [ + `Lens formulas let you do math using a combination of Elasticsearch aggregations and math functions. There are three main types of functions: * Elasticsearch metrics, like \`sum(bytes)\` @@ -30,8 +29,8 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { kql='datacenter.name: east*' )) \`\`\` - - Elasticsearch functions take a field name, which can be in quotes. \`sum(bytes)\` is the same + `, + `Elasticsearch functions take a field name, which can be in quotes. \`sum(bytes)\` is the same as \`sum('bytes')\`. Some functions take named arguments, like \`moving_average(count(), window=5)\`. @@ -42,25 +41,23 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Math functions can take positional arguments, like pow(count(), 3) is the same as count() * count() * count() - Use the symbols +, -, /, and * to perform basic math.`), + Use the symbols +, -, /, and * to perform basic math.`, + ], }, { id: 'lens_common_formulas', - text: dedent(`## Common formulas - - The most common formulas are dividing two values to produce a percent. To display accurately, set - "value format" to "percent". - - ### Filter ratio: + texts: [ + `The most common formulas are dividing two values to produce a percent. To display accurately, set + "value format" to "percent"`, + `### Filter ratio: Use \`kql=''\` to filter one set of documents and compare it to other documents within the same grouping. For example, to see how the error rate changes over time: \`\`\` count(kql='response.status_code > 400') / count() - \`\`\` - - ### Week over week: + \`\`\``, + `### Week over week: Use \`shift='1w'\` to get the value of each grouping from the previous week. Time shift should not be used with the *Top values* function. @@ -68,18 +65,18 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { \`\`\` percentile(system.network.in.bytes, percentile=99) / percentile(system.network.in.bytes, percentile=99, shift='1w') - \`\`\` + \`\`\``, - ### Percent of total + `### Percent of total Formulas can calculate \`overall_sum\` for all the groupings, which lets you convert each grouping into a percent of total: \`\`\` sum(products.base_price) / overall_sum(sum(products.base_price)) - \`\`\` + \`\`\``, - ### Recent change + `### Recent change Use \`reducedTimeRange='30m'\` to add an additional filter on the time range of a metric aligned with the end of the global time range. @@ -88,27 +85,28 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { \`\`\` max(system.network.in.bytes, reducedTimeRange="30m") - min(system.network.in.bytes, reducedTimeRange="30m") - \`\`\` - - `), + \`\`\` + `, + ], }, { id: 'lens_formulas_elasticsearch_functions', - text: dedent(`## Elasticsearch functions + texts: [ + `## Elasticsearch functions These functions will be executed on the raw documents for each row of the resulting table, aggregating all documents matching the break down - dimensions into a single value. + dimensions into a single value.`, - #### average(field: string) + `#### average(field: string) Returns the average of a field. This function only works for number fields. Example: Get the average of price: \`average(price)\` Example: Get the average of price for orders from the UK: \`average(price, - kql='location:UK')\` + kql='location:UK')\``, - #### count([field: string]) + `#### count([field: string]) The total number of documents. When you provide a field, the total number of field values is counted. When you use the Count function for fields that have multiple values in a single document, all values are counted. @@ -118,57 +116,57 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { To calculate the number of products in all orders, use \`count(products.id)\`. To calculate the number of documents that match a specific filter, use - \`count(kql='price > 500')\`. + \`count(kql='price > 500')\`.`, - #### last_value(field: string) + `#### last_value(field: string) Returns the value of a field from the last document, ordered by the default time field of the data view. This function is usefull the retrieve the latest state of an entity. Example: Get the current status of server A: \`last_value(server.status, - kql='server.name="A"')\` + kql='server.name="A"')\``, - #### max(field: string) + `#### max(field: string) Returns the max of a field. This function only works for number fields. Example: Get the max of price: \`max(price)\` Example: Get the max of price for orders from the UK: \`max(price, - kql='location:UK')\` + kql='location:UK')\``, - #### median(field: string) + `#### median(field: string) Returns the median of a field. This function only works for number fields. Example: Get the median of price: \`median(price)\` Example: Get the median of price for orders from the UK: \`median(price, - kql='location:UK')\` + kql='location:UK')\``, - #### min(field: string) + `#### min(field: string) Returns the min of a field. This function only works for number fields. Example: Get the min of price: \`min(price)\` Example: Get the min of price for orders from the UK: \`min(price, - kql='location:UK')\` + kql='location:UK')\``, - #### percentile(field: string, [percentile]: number) + `#### percentile(field: string, [percentile]: number) Returns the specified percentile of the values of a field. This is the value n percent of the values occuring in documents are smaller. Example: Get the number of bytes larger than 95 % of values: - \`percentile(bytes, percentile=95)\` + \`percentile(bytes, percentile=95)\``, - #### percentile_rank(field: string, [value]: number) + `#### percentile_rank(field: string, [value]: number) Returns the percentage of values which are below a certain value. For example, if a value is greater than or equal to 95% of the observed values it is said to be at the 95th percentile rank Example: Get the percentage of values which are below of 100: - \`percentile_rank(bytes, value=100)\` + \`percentile_rank(bytes, value=100)\``, - #### standard_deviation(field: string) + `#### standard_deviation(field: string) Returns the amount of variation or dispersion of the field. The function works only for number fields. @@ -176,17 +174,17 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { \`standard_deviation(price).\` Example: To get the variance of price for orders from the UK, use - \`square(standard_deviation(price, kql='location:UK'))\`. + \`square(standard_deviation(price, kql='location:UK'))\`.`, - #### sum(field: string) + `#### sum(field: string) Returns the sum of a field. This function only works for number fields. Example: Get the sum of price: sum(price) Example: Get the sum of price for orders from the UK: \`sum(price, - kql='location:UK')\` + kql='location:UK')\``, - #### unique_count(field: string) + `#### unique_count(field: string) Calculates the number of unique values of a specified field. Works for number, string, date and boolean values. @@ -196,15 +194,17 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Example: Calculate the number of different products from the "clothes" group: \`unique_count(product.name, kql='product.group=clothes')\` - `), + `, + ], }, { id: 'lens_formulas_column_functions', - text: dedent(`## Column calculations + texts: [ + `## Column calculations These functions are executed for each row, but are provided with the whole - column as context. This is also known as a window function. - - #### counter_rate(metric: number) + column as context. This is also known as a window function.`, + + `#### counter_rate(metric: number) Calculates the rate of an ever increasing counter. This function will only yield helpful results on counter metric fields which contain a measurement of some kind monotonically growing over time. If the value does get smaller, it @@ -215,9 +215,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { or top values dimensions. It uses the current interval when used in Formula. Example: Visualize the rate of bytes received over time by a memcached server: - counter_rate(max(memcached.stats.read.bytes)) - - cumulative_sum(metric: number) + counter_rate(max(memcached.stats.read.bytes))`, + + `cumulative_sum(metric: number) Calculates the cumulative sum of a metric over time, adding all previous values of a series to each value. To use this function, you need to configure a date histogram dimension as well. @@ -226,9 +226,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { or top values dimensions. Example: Visualize the received bytes accumulated over time: - cumulative_sum(sum(bytes)) - - differences(metric: number) + cumulative_sum(sum(bytes))`, + + `differences(metric: number) Calculates the difference to the last value of a metric over time. To use this function, you need to configure a date histogram dimension as well. Differences requires the data to be sequential. If your data is empty when using @@ -238,9 +238,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { or top values dimensions. Example: Visualize the change in bytes received over time: - differences(sum(bytes)) - - moving_average(metric: number, [window]: number) + differences(sum(bytes))`, + + `moving_average(metric: number, [window]: number) Calculates the moving average of a metric over time, averaging the last n-th values to calculate the current value. To use this function, you need to configure a date histogram dimension as well. The default window value is 5. @@ -251,9 +251,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Takes a named parameter window which specifies how many last values to include in the average calculation for the current value. - Example: Smooth a line of measurements: moving_average(sum(bytes), window=5) - - normalize_by_unit(metric: number, unit: s|m|h|d|w|M|y) + Example: Smooth a line of measurements: moving_average(sum(bytes), window=5)`, + + `normalize_by_unit(metric: number, unit: s|m|h|d|w|M|y) This advanced function is useful for normalizing counts and sums to a specific time interval. It allows for integration with metrics that are stored already normalized to a specific time interval. @@ -264,9 +264,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Example: A ratio comparing an already normalized metric to another metric that needs to be normalized. normalize_by_unit(counter_rate(max(system.diskio.write.bytes)), unit='s') / - last_value(apache.status.bytes_per_second) - - overall_average(metric: number) + last_value(apache.status.bytes_per_second)`, + + `overall_average(metric: number) Calculates the average of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function. Other dimensions breaking down the data like top values or @@ -276,9 +276,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { overall_average is calculating the average over all dimensions no matter the used function - Example: Divergence from the mean: sum(bytes) - overall_average(sum(bytes)) - - overall_max(metric: number) + Example: Divergence from the mean: sum(bytes) - overall_average(sum(bytes))`, + + `overall_max(metric: number) Calculates the maximum of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function. Other dimensions breaking down the data like top values or @@ -289,9 +289,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { function Example: Percentage of range (sum(bytes) - overall_min(sum(bytes))) / - (overall_max(sum(bytes)) - overall_min(sum(bytes))) - - overall_min(metric: number) + (overall_max(sum(bytes)) - overall_min(sum(bytes)))`, + + `overall_min(metric: number) Calculates the minimum of a metric for all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function. Other dimensions breaking down the data like top values or @@ -302,9 +302,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { function Example: Percentage of range (sum(bytes) - overall_min(sum(bytes)) / - (overall_max(sum(bytes)) - overall_min(sum(bytes))) - - overall_sum(metric: number) + (overall_max(sum(bytes)) - overall_min(sum(bytes)))`, + + `overall_sum(metric: number) Calculates the sum of a metric of all data points of a series in the current chart. A series is defined by a dimension using a date histogram or interval function. Other dimensions breaking down the data like top values or filter are @@ -314,19 +314,21 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { overall_sum is calculating the sum over all dimensions no matter the used function. - Example: Percentage of total sum(bytes) / overall_sum(sum(bytes))`), + Example: Percentage of total sum(bytes) / overall_sum(sum(bytes))`, + ], }, { id: 'lens_formulas_math_functions', - text: dedent(`Math - These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions. - - abs([value]: number) + texts: [ + `Math + These functions will be executed for reach row of the resulting table using single values from the same row calculated using other functions.`, + + `abs([value]: number) Calculates absolute value. A negative value is multiplied by -1, a positive value stays the same. - Example: Calculate average distance to sea level abs(average(altitude)) - - add([left]: number, [right]: number) + Example: Calculate average distance to sea level abs(average(altitude))`, + + `add([left]: number, [right]: number) Adds up two numbers. Also works with + symbol. @@ -337,9 +339,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Example: Offset count by a static value - add(count(), 5) - - cbrt([value]: number) + add(count(), 5)`, + + `cbrt([value]: number) Cube root of value. Example: Calculate side length from volume @@ -351,9 +353,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Example: Round up price to the next dollar - ceil(sum(price)) - - clamp([value]: number, [min]: number, [max]: number) + ceil(sum(price))`, + + `clamp([value]: number, [min]: number, [max]: number) Limits the value from a minimum to maximum. Example: Make sure to catch outliers @@ -362,22 +364,22 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { average(bytes), percentile(bytes, percentile=5), percentile(bytes, percentile=95) - ) - cube([value]: number) + )`, + `cube([value]: number) Calculates the cube of a number. Example: Calculate volume from side length - cube(last_value(length)) - - defaults([value]: number, [default]: number) + cube(last_value(length))`, + + `defaults([value]: number, [default]: number) Returns a default numeric value when value is null. Example: Return -1 when a field has no data - defaults(average(bytes), -1) - - divide([left]: number, [right]: number) + defaults(average(bytes), -1)`, + + `divide([left]: number, [right]: number) Divides the first number by the second number. Also works with / symbol @@ -386,44 +388,44 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { sum(profit) / sum(revenue) - Example: divide(sum(bytes), 2) - - exp([value]: number) + Example: divide(sum(bytes), 2)`, + + `exp([value]: number) Raises e to the nth power. Example: Calculate the natural exponential function - exp(last_value(duration)) - - fix([value]: number) + exp(last_value(duration))`, + + `fix([value]: number) For positive values, takes the floor. For negative values, takes the ceiling. Example: Rounding towards zero - fix(sum(profit)) - - floor([value]: number) + fix(sum(profit))`, + + `floor([value]: number) Round down to nearest integer value Example: Round down a price - floor(sum(price)) - - log([value]: number, [base]?: number) + floor(sum(price))`, + + `log([value]: number, [base]?: number) Logarithm with optional base. The natural base e is used as default. Example: Calculate number of bits required to store values log(sum(bytes)) - log(sum(bytes), 2) - mod([value]: number, [base]: number) + log(sum(bytes), 2)`, + `mod([value]: number, [base]: number) Remainder after dividing the function by a number Example: Calculate last three digits of a value - mod(sum(price), 1000) - - multiply([left]: number, [right]: number) + mod(sum(price), 1000)`, + + `multiply([left]: number, [right]: number) Multiplies two numbers. Also works with * symbol. @@ -434,63 +436,67 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { Example: Calculate price after constant tax rate - multiply(sum(price), 1.2) - - pick_max([left]: number, [right]: number) + multiply(sum(price), 1.2)`, + + `pick_max([left]: number, [right]: number) Finds the maximum value between two numbers. Example: Find the maximum between two fields averages - pick_max(average(bytes), average(memory)) - - pick_min([left]: number, [right]: number) + pick_max(average(bytes), average(memory))`, + + `pick_min([left]: number, [right]: number) Finds the minimum value between two numbers. Example: Find the minimum between two fields averages - pick_min(average(bytes), average(memory)) - - pow([value]: number, [base]: number) + pick_min(average(bytes), average(memory))`, + + `pow([value]: number, [base]: number) Raises the value to a certain power. The second argument is required Example: Calculate volume based on side length - pow(last_value(length), 3) - - round([value]: number, [decimals]?: number) + pow(last_value(length), 3)`, + + `round([value]: number, [decimals]?: number) Rounds to a specific number of decimal places, default of 0 Examples: Round to the cent round(sum(bytes)) - round(sum(bytes), 2) - sqrt([value]: number) + round(sum(bytes), 2)`, + `sqrt([value]: number) Square root of a positive value only Example: Calculate side length based on area - sqrt(last_value(area)) - - square([value]: number) + sqrt(last_value(area))`, + + `square([value]: number) Raise the value to the 2nd power Example: Calculate area based on side length - square(last_value(length)) - - subtract([left]: number, [right]: number) + square(last_value(length))`, + + `subtract([left]: number, [right]: number) Subtracts the first number from the second number. Also works with - symbol. Example: Calculate the range of a field - subtract(max(bytes), min(bytes)) - - Comparison - These functions are used to perform value comparison. - - eq([left]: number, [right]: number) + subtract(max(bytes), min(bytes))`, + ], + }, + { + id: 'lens_formulas_comparison_functions', + texts: [ + `Comparison + These functions are used to perform value comparison.`, + + `eq([left]: number, [right]: number) Performs an equality comparison between two values. To be used as condition for ifelse comparison function. @@ -501,9 +507,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { average(bytes) == average(memory) - Example: eq(sum(bytes), 1000000) - - gt([left]: number, [right]: number) + Example: eq(sum(bytes), 1000000)`, + + `gt([left]: number, [right]: number) Performs a greater than comparison between two values. To be used as condition for ifelse comparison function. @@ -514,9 +520,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { average(bytes) > average(memory) - Example: gt(average(bytes), 1000) - - gte([left]: number, [right]: number) + Example: gt(average(bytes), 1000)`, + + `gte([left]: number, [right]: number) Performs a greater than comparison between two values. To be used as condition for ifelse comparison function. @@ -527,16 +533,16 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { average(bytes) >= average(memory) - Example: gte(average(bytes), 1000) - - ifelse([condition]: boolean, [left]: number, [right]: number) + Example: gte(average(bytes), 1000)`, + + `ifelse([condition]: boolean, [left]: number, [right]: number) Returns a value depending on whether the element of condition is true or false. Example: Average revenue per customer but in some cases customer id is not provided which counts as additional customer - sum(total)/(unique_count(customer_id) + ifelse( count() > count(kql='customer_id:*'), 1, 0)) - - lt([left]: number, [right]: number) + sum(total)/(unique_count(customer_id) + ifelse( count() > count(kql='customer_id:*'), 1, 0))`, + + `lt([left]: number, [right]: number) Performs a lower than comparison between two values. To be used as condition for ifelse comparison function. @@ -547,9 +553,9 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { average(bytes) <= average(memory) - Example: lt(average(bytes), 1000) - - lte([left]: number, [right]: number) + Example: lt(average(bytes), 1000)`, + + `lte([left]: number, [right]: number) Performs a lower than or equal comparison between two values. To be used as condition for ifelse comparison function. @@ -560,7 +566,8 @@ export function addLensDocsToKb(service: ObservabilityAIAssistantService) { average(bytes) <= average(memory) - Example: lte(average(bytes), 1000)`), + Example: lte(average(bytes), 1000)`, + ], }, { id: 'lens_formulas_kibana_context', diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/split_kb_text.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/split_kb_text.ts new file mode 100644 index 00000000000000..e2b5b7c2c5784c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/split_kb_text.ts @@ -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 { merge } from 'lodash'; +import type { KnowledgeBaseEntry } from '../../../common/types'; +import { type KnowledgeBaseEntryOperation, KnowledgeBaseEntryOperationType } from '../kb_service'; + +export function splitKbText({ + id, + texts, + ...rest +}: Omit & { texts: string[] }): KnowledgeBaseEntryOperation[] { + return [ + { + type: KnowledgeBaseEntryOperationType.Delete, + labels: { + document_id: id, + }, + }, + ...texts.map((text, index) => ({ + type: KnowledgeBaseEntryOperationType.Index, + document: merge({}, rest, { + id: [id, index].join('_'), + labels: { + document_id: id, + }, + text, + }), + })), + ]; +} diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index 8c49398e2deab9..ddbd38c21bc5de 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -42,7 +42,9 @@ "@kbn/field-formats-plugin", "@kbn/lens-plugin", "@kbn/data-views-plugin", - "@kbn/task-manager-plugin" + "@kbn/task-manager-plugin", + "@kbn/es-query", + "@kbn/rule-registry-plugin" ], "exclude": ["target/**/*"] }
+ + + {children} + + +
+
+ + {children} + +
+