diff --git a/.i18nrc.json b/.i18nrc.json index 4bcb014ebf56e..cca959f8e0005 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -136,6 +136,7 @@ "visTypeXy": "src/plugins/vis_types/xy", "visualizations": "src/plugins/visualizations", "visualizationUiComponents": "packages/kbn-visualization-ui-components", + "visualizationUtils": "packages/kbn-visualization-utils", "unifiedDocViewer": ["src/plugins/unified_doc_viewer", "packages/kbn-unified-doc-viewer"], "unifiedSearch": "src/plugins/unified_search", "unifiedFieldList": "packages/kbn-unified-field-list", diff --git a/packages/kbn-field-types/index.ts b/packages/kbn-field-types/index.ts index 62ef4b011d467..c5c5f7cba502d 100644 --- a/packages/kbn-field-types/index.ts +++ b/packages/kbn-field-types/index.ts @@ -13,6 +13,7 @@ export { getKbnFieldType, getKbnTypeNames, getFilterableKbnTypeNames, + esFieldTypeToKibanaFieldType, } from './src/kbn_field_types'; export type { KbnFieldTypeOptions } from './src/types'; diff --git a/packages/kbn-field-types/src/kbn_field_types.ts b/packages/kbn-field-types/src/kbn_field_types.ts index 21d1550ce3566..2d03d3d1db782 100644 --- a/packages/kbn-field-types/src/kbn_field_types.ts +++ b/packages/kbn-field-types/src/kbn_field_types.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { createKbnFieldTypes, kbnFieldTypeUnknown } from './kbn_field_types_factory'; import { KbnFieldType } from './kbn_field_type'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from './types'; @@ -49,3 +48,18 @@ export const castEsToKbnFieldTypeName = (esType: ES_FIELD_TYPES | string): KBN_F */ export const getFilterableKbnTypeNames = (): string[] => registeredKbnTypes.filter((type) => type.filterable).map((type) => type.name); + +export function esFieldTypeToKibanaFieldType(type: string) { + switch (type) { + case ES_FIELD_TYPES._INDEX: + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.IP: + return KBN_FIELD_TYPES.STRING; + case '_version': + return KBN_FIELD_TYPES.NUMBER; + case 'datetime': + return KBN_FIELD_TYPES.DATE; + default: + return castEsToKbnFieldTypeName(type); + } +} diff --git a/packages/kbn-visualization-utils/__mocks__/suggestions_mock.ts b/packages/kbn-visualization-utils/__mocks__/suggestions_mock.ts new file mode 100644 index 0000000000000..f8f765b14a64f --- /dev/null +++ b/packages/kbn-visualization-utils/__mocks__/suggestions_mock.ts @@ -0,0 +1,134 @@ +/* + * 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 type { TableChangeType } from '../src/types'; + +export const currentSuggestionMock = { + title: 'Heat map', + hide: false, + score: 0.6, + previewIcon: 'heatmap', + visualizationId: 'lnsHeatmap', + visualizationState: { + shape: 'heatmap', + layerId: '46aa21fa-b747-4543-bf90-0b40007c546d', + layerType: 'data', + legend: { + isVisible: true, + position: 'right', + type: 'heatmap_legend', + }, + gridConfig: { + type: 'heatmap_grid', + isCellLabelVisible: false, + isYAxisLabelVisible: true, + isXAxisLabelVisible: true, + isYAxisTitleVisible: false, + isXAxisTitleVisible: false, + }, + valueAccessor: '5b9b8b76-0836-4a12-b9c0-980c9900502f', + xAccessor: '81e332d6-ee37-42a8-a646-cea4fc75d2d3', + }, + keptLayerIds: ['46aa21fa-b747-4543-bf90-0b40007c546d'], + datasourceState: { + layers: { + '46aa21fa-b747-4543-bf90-0b40007c546d': { + index: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + query: { + esql: 'FROM kibana_sample_data_flights | keep Dest, AvgTicketPrice', + }, + columns: [ + { + columnId: '81e332d6-ee37-42a8-a646-cea4fc75d2d3', + fieldName: 'Dest', + meta: { + type: 'string', + }, + }, + { + columnId: '5b9b8b76-0836-4a12-b9c0-980c9900502f', + fieldName: 'AvgTicketPrice', + meta: { + type: 'number', + }, + }, + ], + timeField: 'timestamp', + }, + }, + indexPatternRefs: [], + initialContext: { + dataViewSpec: { + id: 'd3d7af60-4c81-11e8-b3d7-01146121b73d', + version: 'WzM1ODA3LDFd', + title: 'kibana_sample_data_flights', + timeFieldName: 'timestamp', + sourceFilters: [], + fields: { + AvgTicketPrice: { + count: 0, + name: 'AvgTicketPrice', + type: 'number', + esTypes: ['float'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: { + id: 'number', + params: { + pattern: '$0,0.[00]', + }, + }, + shortDotsEnable: false, + isMapped: true, + }, + Dest: { + count: 0, + name: 'Dest', + type: 'string', + esTypes: ['keyword'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: { + id: 'string', + }, + shortDotsEnable: false, + isMapped: true, + }, + timestamp: { + count: 0, + name: 'timestamp', + type: 'date', + esTypes: ['date'], + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + format: { + id: 'date', + }, + shortDotsEnable: false, + isMapped: true, + }, + }, + allowNoIndex: false, + name: 'Kibana Sample Data Flights', + }, + fieldName: '', + contextualFields: ['Dest', 'AvgTicketPrice'], + query: { + esql: 'FROM "kibana_sample_data_flights"', + }, + }, + }, + datasourceId: 'textBased', + columns: 2, + changeType: 'initial' as TableChangeType, +}; diff --git a/packages/kbn-visualization-utils/index.ts b/packages/kbn-visualization-utils/index.ts index 1665599e93d54..8298260320564 100644 --- a/packages/kbn-visualization-utils/index.ts +++ b/packages/kbn-visualization-utils/index.ts @@ -7,4 +7,5 @@ */ export { getTimeZone } from './src/get_timezone'; +export { getLensAttributesFromSuggestion } from './src/get_lens_attributes'; export { TooltipWrapper } from './src/tooltip_wrapper'; diff --git a/packages/kbn-visualization-utils/src/get_lens_attributes.test.ts b/packages/kbn-visualization-utils/src/get_lens_attributes.test.ts new file mode 100644 index 0000000000000..94e0b8e752926 --- /dev/null +++ b/packages/kbn-visualization-utils/src/get_lens_attributes.test.ts @@ -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 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 { getLensAttributesFromSuggestion } from './get_lens_attributes'; +import { AggregateQuery } from '@kbn/es-query'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import { currentSuggestionMock } from '../__mocks__/suggestions_mock'; + +describe('getLensAttributesFromSuggestion', () => { + const dataView = { + id: `index-pattern-with-timefield-id`, + title: `index-pattern-with-timefield-title`, + fields: [], + getFieldByName: jest.fn(), + timeFieldName: '@timestamp', + isPersisted: () => false, + toSpec: () => ({}), + } as unknown as DataView; + const query: AggregateQuery = { esql: 'from foo | limit 10' }; + + it('should return correct attributes for given suggestion', () => { + const lensAttrs = getLensAttributesFromSuggestion({ + filters: [], + query, + dataView, + suggestion: currentSuggestionMock, + }); + expect(lensAttrs).toEqual({ + state: expect.objectContaining({ + adHocDataViews: { + 'index-pattern-with-timefield-id': {}, + }, + }), + references: [ + { + id: 'index-pattern-with-timefield-id', + name: 'textBasedLanguages-datasource-layer-suggestion', + type: 'index-pattern', + }, + ], + title: currentSuggestionMock.title, + visualizationType: 'lnsHeatmap', + }); + }); +}); diff --git a/packages/kbn-visualization-utils/src/get_lens_attributes.ts b/packages/kbn-visualization-utils/src/get_lens_attributes.ts new file mode 100644 index 0000000000000..38a2dd29b841e --- /dev/null +++ b/packages/kbn-visualization-utils/src/get_lens_attributes.ts @@ -0,0 +1,64 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { AggregateQuery, Query, Filter } from '@kbn/es-query'; +import type { Suggestion } from './types'; + +export const getLensAttributesFromSuggestion = ({ + filters, + query, + suggestion, + dataView, +}: { + filters: Filter[]; + query: Query | AggregateQuery; + suggestion: Suggestion | undefined; + dataView?: DataView; +}) => { + const suggestionDatasourceState = Object.assign({}, suggestion?.datasourceState); + const suggestionVisualizationState = Object.assign({}, suggestion?.visualizationState); + const datasourceStates = + suggestion && suggestion.datasourceState + ? { + [suggestion.datasourceId!]: { + ...suggestionDatasourceState, + }, + } + : { + formBased: {}, + }; + const visualization = suggestionVisualizationState; + const attributes = { + title: suggestion + ? suggestion.title + : i18n.translate('visualizationUtils.config.suggestion.title', { + defaultMessage: 'New suggestion', + }), + references: [ + { + id: dataView?.id ?? '', + name: `textBasedLanguages-datasource-layer-suggestion`, + type: 'index-pattern', + }, + ], + state: { + datasourceStates, + filters, + query, + visualization, + ...(dataView && + dataView.id && + !dataView.isPersisted() && { + adHocDataViews: { [dataView.id]: dataView.toSpec(false) }, + }), + }, + visualizationType: suggestion ? suggestion.visualizationId : 'lnsXY', + }; + return attributes; +}; diff --git a/packages/kbn-visualization-utils/src/types.ts b/packages/kbn-visualization-utils/src/types.ts new file mode 100644 index 0000000000000..42c2bce2fdbda --- /dev/null +++ b/packages/kbn-visualization-utils/src/types.ts @@ -0,0 +1,43 @@ +/* + * 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 type { Ast } from '@kbn/interpreter'; +import type { IconType } from '@elastic/eui/src/components/icon/icon'; + +/** + * Indicates what was changed in this table compared to the currently active table of this layer. + * * `initial` means the layer associated with this table does not exist in the current configuration + * * `unchanged` means the table is the same in the currently active configuration + * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) + * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) + * * `reorder` means the table columns have changed order, which change the data as well + * * `layers` means the change is a change to the layer structure, not to the table + */ +export type TableChangeType = + | 'initial' + | 'unchanged' + | 'reduced' + | 'extended' + | 'reorder' + | 'layers'; + +export interface Suggestion { + visualizationId: string; + datasourceState?: V; + datasourceId?: string; + columns: number; + score: number; + title: string; + visualizationState: T; + previewExpression?: Ast | string; + previewIcon: IconType; + hide?: boolean; + // flag to indicate if the visualization is incomplete + incomplete?: boolean; + changeType: TableChangeType; + keptLayerIds: string[]; +} diff --git a/packages/kbn-visualization-utils/tsconfig.json b/packages/kbn-visualization-utils/tsconfig.json index 1afc36bf0b0be..5c2e71305c3fe 100644 --- a/packages/kbn-visualization-utils/tsconfig.json +++ b/packages/kbn-visualization-utils/tsconfig.json @@ -9,5 +9,9 @@ ], "kbn_references": [ "@kbn/core", + "@kbn/i18n", + "@kbn/interpreter", + "@kbn/data-views-plugin", + "@kbn/es-query", ] } diff --git a/src/plugins/data/common/search/expressions/essql.ts b/src/plugins/data/common/search/expressions/essql.ts index b27d9137445f6..8c5baa66f9f53 100644 --- a/src/plugins/data/common/search/expressions/essql.ts +++ b/src/plugins/data/common/search/expressions/essql.ts @@ -8,13 +8,9 @@ import type { KibanaRequest } from '@kbn/core/server'; import { buildEsQuery } from '@kbn/es-query'; -import { castEsToKbnFieldTypeName, ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; +import { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; import { i18n } from '@kbn/i18n'; -import type { - Datatable, - DatatableColumnType, - ExpressionFunctionDefinition, -} from '@kbn/expressions-plugin/common'; +import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { zipObject } from 'lodash'; @@ -61,21 +57,6 @@ interface EssqlStartDependencies { uiSettings: UiSettingsCommon; } -function normalizeType(type: string): DatatableColumnType { - switch (type) { - case ES_FIELD_TYPES._INDEX: - case ES_FIELD_TYPES.GEO_POINT: - case ES_FIELD_TYPES.IP: - return KBN_FIELD_TYPES.STRING; - case '_version': - return KBN_FIELD_TYPES.NUMBER; - case 'datetime': - return KBN_FIELD_TYPES.DATE; - default: - return castEsToKbnFieldTypeName(type) as DatatableColumnType; - } -} - function sanitize(value: string) { return value.replace(/[\(\)]/g, '_'); } @@ -260,7 +241,7 @@ export const getEssqlFn = ({ getStartDependencies }: EssqlFnArguments) => { body.columns?.map(({ name, type }) => ({ id: sanitize(name), name: sanitize(name), - meta: { type: normalizeType(type) }, + meta: { type: esFieldTypeToKibanaFieldType(type) }, })) ?? []; const columnNames = columns.map(({ name }) => name); const rows = body.rows.map((row) => zipObject(columnNames, row)); diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx index 0944326cf43c2..1ac76928e011c 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/get_edit_lens_configuration.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useState } from 'react'; import { EuiFlyout, EuiLoadingSpinner, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n-react'; import { Provider } from 'react-redux'; import type { MiddlewareAPI, Dispatch, Action } from '@reduxjs/toolkit'; import { css } from '@emotion/react'; @@ -220,11 +221,13 @@ export async function getEditLensConfiguration( return getWrapper( - - - - - + + + + + + + ); }; diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx index 106eeee037704..834929d4ca2a5 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/lens_configuration_flyout.tsx @@ -200,34 +200,36 @@ export function LensEditConfigurationFlyout({ ]); const onApply = useCallback(() => { + const dsStates = Object.fromEntries( + Object.entries(datasourceStates).map(([id, ds]) => { + const dsState = ds.state; + return [id, dsState]; + }) + ); + const references = extractReferencesFromState({ + activeDatasources: Object.keys(datasourceStates).reduce( + (acc, id) => ({ + ...acc, + [id]: datasourceMap[id], + }), + {} + ), + datasourceStates, + visualizationState: visualization.state, + activeVisualization, + }); + const attrs = { + ...attributes, + state: { + ...attributes.state, + visualization: visualization.state, + datasourceStates: dsStates, + }, + references, + visualizationType: visualization.activeId, + title: visualization.activeId ?? '', + }; if (savedObjectId) { - const dsStates = Object.fromEntries( - Object.entries(datasourceStates).map(([id, ds]) => { - const dsState = ds.state; - return [id, dsState]; - }) - ); - const references = extractReferencesFromState({ - activeDatasources: Object.keys(datasourceStates).reduce( - (acc, id) => ({ - ...acc, - [id]: datasourceMap[id], - }), - {} - ), - datasourceStates, - visualizationState: visualization.state, - activeVisualization, - }); - const attrs = { - ...attributes, - state: { - ...attributes.state, - visualization: visualization.state, - datasourceStates: dsStates, - }, - references, - }; saveByRef?.(attrs); updateByRefInput?.(savedObjectId); } @@ -245,7 +247,7 @@ export function LensEditConfigurationFlyout({ trackUiCounterEvents(telemetryEvents); } - onApplyCb?.(); + onApplyCb?.(attrs as TypedLensByValueInput['attributes']); closeFlyout?.(); }, [ visualization.activeId, @@ -256,9 +258,9 @@ export function LensEditConfigurationFlyout({ visualization.state, activeVisualization, attributes, + datasourceMap, saveByRef, updateByRefInput, - datasourceMap, ]); const { getUserMessages } = useApplicationUserMessages({ diff --git a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts index bb6b1157f43b9..4439cb1b791c1 100644 --- a/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/shared/edit_on_the_fly/types.ts @@ -83,7 +83,7 @@ export interface EditConfigPanelProps { /** If set to true the layout changes to accordion and the text based query (i.e. ES|QL) can be edited */ hidesSuggestions?: boolean; /** Optional callback for apply flyout button */ - onApplyCb?: () => void; + onApplyCb?: (input: TypedLensByValueInput['attributes']) => void; /** Optional callback for cancel flyout button */ onCancelCb?: () => void; } diff --git a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx index dbac941720b4d..7245201cf3322 100644 --- a/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx +++ b/x-pack/plugins/lens/public/mocks/lens_plugin_mock.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { createFormulaPublicApi } from '../async_services'; import { LensPublicStart } from '..'; import { visualizationTypes } from '../visualizations/xy/types'; +import { mockAllSuggestions } from './suggestions_mock'; type Start = jest.Mocked; @@ -32,6 +33,7 @@ export const lensPluginMock = { stateHelperApi: jest.fn().mockResolvedValue({ formula: createFormulaPublicApi(), + suggestions: jest.fn().mockReturnValue(mockAllSuggestions), }), }; return startContract; diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 2431676b8dadd..daf6ae44aacaa 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -348,7 +348,9 @@ export function loadInitial( !(initialInput as LensByReferenceInput)?.savedObjectId && currentSessionId ? currentSessionId - : data.search.session.start(), + : !inlineEditing + ? data.search.session.start() + : undefined, persistedDoc: doc, activeDatasourceId: getInitialDatasourceId(datasourceMap, doc), visualization: { diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx index 584aa7aaf132a..6b3c020e40d97 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/in_app_embeddable_edit_action_helpers.tsx @@ -40,7 +40,7 @@ export async function executeEditEmbeddableAction({ lensEvent: LensChartLoadEvent; container?: HTMLElement | null; onUpdate: (newAttributes: TypedLensByValueInput['attributes']) => void; - onApply?: () => void; + onApply?: (newAttributes: TypedLensByValueInput['attributes']) => void; onCancel?: () => void; }) { const isCompatibleAction = isEmbeddableEditActionCompatible(core, attributes); diff --git a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts index e1eadf7a32660..d86f05d4156e9 100644 --- a/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts +++ b/x-pack/plugins/lens/public/trigger_actions/open_lens_config/in_app_embeddable_edit/types.ts @@ -28,7 +28,7 @@ export interface InlineEditLensEmbeddableContext { // callback which runs every time something changes in the dimension panel onUpdate: (newAttributes: TypedLensByValueInput['attributes']) => void; // optional onApply callback - onApply?: () => void; + onApply?: (newAttributes: TypedLensByValueInput['attributes']) => void; // optional onCancel callback onCancel?: () => void; // custom container element, use in case you need to render outside a flyout diff --git a/x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx b/x-pack/plugins/observability_ai_assistant/common/functions/lens.ts similarity index 97% rename from x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx rename to x-pack/plugins/observability_ai_assistant/common/functions/lens.ts index a3d8487a83b8a..c466b4e422f2f 100644 --- a/x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx +++ b/x-pack/plugins/observability_ai_assistant/common/functions/lens.ts @@ -7,6 +7,7 @@ import { FromSchema } from 'json-schema-to-ts'; import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common'; +import { FunctionVisibility } from '../types'; export enum SeriesType { Bar = 'bar', @@ -23,6 +24,8 @@ export enum SeriesType { export const lensFunctionDefinition = { name: 'lens', contexts: ['core'], + // function is deprecated + visibility: FunctionVisibility.Internal, description: "Use this function to create custom visualizations, using Lens, that can be saved to dashboards. This function does not return data to the assistant, it only shows it to the user. When using this function, make sure to use the recall function to get more information about how to use it, with how you want to use it. Make sure the query also contains information about the user's request. The visualisation is displayed to the user above your reply, DO NOT try to generate or display an image yourself.", descriptionForUser: diff --git a/x-pack/plugins/observability_ai_assistant/common/functions/visualize_esql.ts b/x-pack/plugins/observability_ai_assistant/common/functions/visualize_esql.ts new file mode 100644 index 0000000000000..e495f47a552bf --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/common/functions/visualize_esql.ts @@ -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 { FromSchema } from 'json-schema-to-ts'; +import { FunctionVisibility } from '../types'; + +export enum VisualizeESQLUserIntention { + generateQueryOnly = 'generateQueryOnly', + executeAndReturnResults = 'executeAndReturnResults', + visualizeAuto = 'visualizeAuto', + visualizeXy = 'visualizeXy', + visualizeBar = 'visualizeBar', + visualizeLine = 'visualizeLine', + visualizeDonut = 'visualizeDonut', + visualizeTreemap = 'visualizeTreemap', + visualizeHeatmap = 'visualizeHeatmap', + visualizeTagcloud = 'visualizeTagcloud', + visualizeWaffle = 'visualizeWaffle', +} + +export const VISUALIZE_ESQL_USER_INTENTIONS: VisualizeESQLUserIntention[] = Object.values( + VisualizeESQLUserIntention +); + +export const visualizeESQLFunction = { + name: 'visualize_query', + visibility: FunctionVisibility.UserOnly, + description: 'Use this function to visualize charts for ES|QL queries.', + descriptionForUser: 'Use this function to visualize charts for ES|QL queries.', + parameters: { + type: 'object', + additionalProperties: true, + properties: { + query: { + type: 'string', + }, + intention: { + type: 'string', + enum: VISUALIZE_ESQL_USER_INTENTIONS, + }, + }, + required: ['query', 'intention'], + } as const, + contexts: ['core'], +}; + +export type VisualizeESQLFunctionArguments = FromSchema; diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 8b649bf72a55d..f04963a6c5a2c 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -105,8 +105,9 @@ export type FunctionResponse = | Observable; export enum FunctionVisibility { - System = 'system', - User = 'user', + AssistantOnly = 'assistantOnly', + UserOnly = 'userOnly', + Internal = 'internal', All = 'all', } diff --git a/x-pack/plugins/observability_ai_assistant/kibana.jsonc b/x-pack/plugins/observability_ai_assistant/kibana.jsonc index 4024a9361a372..3f346cccff0c1 100644 --- a/x-pack/plugins/observability_ai_assistant/kibana.jsonc +++ b/x-pack/plugins/observability_ai_assistant/kibana.jsonc @@ -20,6 +20,7 @@ "share", "taskManager", "triggersActionsUi", + "uiActions", "dataViews", "ml" ], diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index 119a87554d30e..f11f2c8b56bc6 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React, { useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHeaderLink, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo, useState } from 'react'; import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider'; import { useAbortableAsync } from '../../hooks/use_abortable_async'; import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx index 899e2e4e6074b..4186f2c70c04d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_actions_menu.tsx @@ -9,20 +9,23 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiContextMenu, EuiPanel, EuiPopover } from '@elastic/eui'; import { useKibana } from '../../hooks/use_kibana'; +import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { getSettingsHref } from '../../utils/get_settings_href'; import { getSettingsKnowledgeBaseHref } from '../../utils/get_settings_kb_href'; -import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; import { ConnectorSelectorBase } from '../connector_selector/connector_selector_base'; +import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; export function ChatActionsMenu({ connectors, conversationId, disabled, + showLinkToConversationsApp, onCopyConversationClick, }: { connectors: UseGenAIConnectorsResult; conversationId?: string; disabled: boolean; + showLinkToConversationsApp: boolean; onCopyConversationClick: () => void; }) { const { @@ -31,6 +34,8 @@ export function ChatActionsMenu({ } = useKibana().services; const [isOpen, setIsOpen] = useState(false); + const router = useObservabilityAIAssistantRouter(); + const toggleActionsMenu = () => { setIsOpen(!isOpen); }; @@ -70,6 +75,42 @@ export function ChatActionsMenu({ defaultMessage: 'Actions', }), items: [ + ...(showLinkToConversationsApp + ? [ + { + name: conversationId + ? i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.openInConversationsApp', + { + defaultMessage: 'Open in Conversations app', + } + ) + : i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.goToConversationsApp', + { + defaultMessage: 'Go to Conversations app', + } + ), + href: conversationId + ? router.link('/conversations/{conversationId}', { + path: { conversationId }, + }) + : router.link('/conversations/new'), + }, + ] + : []), + { + name: i18n.translate( + 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase', + { + defaultMessage: 'Manage knowledge base', + } + ), + onClick: () => { + toggleActionsMenu(); + handleNavigateToSettingsKnowledgeBase(); + }, + }, { name: i18n.translate('xpack.observabilityAiAssistant.chatHeader.actions.settings', { defaultMessage: 'AI Assistant Settings', @@ -95,18 +136,6 @@ export function ChatActionsMenu({ ), panel: 1, }, - { - name: i18n.translate( - 'xpack.observabilityAiAssistant.chatHeader.actions.knowledgeBase', - { - defaultMessage: 'Manage knowledge base', - } - ), - onClick: () => { - toggleActionsMenu(); - handleNavigateToSettingsKnowledgeBase(); - }, - }, { name: i18n.translate( 'xpack.observabilityAiAssistant.chatHeader.actions.copyConversation', diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 7906c63e823e2..373e35641ff8f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { css, keyframes } from '@emotion/css'; import { EuiCallOut, @@ -21,6 +21,7 @@ import type { AuthenticatedUser } from '@kbn/security-plugin/common'; import { euiThemeVars } from '@kbn/ui-theme'; import { i18n } from '@kbn/i18n'; import { findLastIndex } from 'lodash'; +import { VisualizeESQLUserIntention } from '../../../common/functions/visualize_esql'; import { ChatState } from '../../hooks/use_chat'; import { useConversation } from '../../hooks/use_conversation'; import { useLicense } from '../../hooks/use_license'; @@ -34,8 +35,12 @@ import { ChatTimeline } from './chat_timeline'; import { Feedback } from '../feedback_buttons'; import { IncorrectLicensePanel } from './incorrect_license_panel'; import { WelcomeMessage } from './welcome_message'; +import { + ChatActionClickHandler, + ChatActionClickType, + type ChatFlyoutSecondSlotHandler, +} from './types'; import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; -import { ChatActionClickType } from './types'; import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { TELEMETRY, sendEvent } from '../../analytics'; @@ -94,7 +99,9 @@ export function ChatBody({ connectors, knowledgeBase, currentUser, + showLinkToConversationsApp, startedFrom, + chatFlyoutSecondSlotHandler, onConversationUpdate, }: { initialTitle?: string; @@ -103,7 +110,9 @@ export function ChatBody({ connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; currentUser?: Pick; + showLinkToConversationsApp: boolean; startedFrom?: StartedFrom; + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void; }) { const license = useLicense(); @@ -171,13 +180,13 @@ export function ChatBody({ } }; - const handleChangeHeight = (editorHeight: number) => { + const handleChangeHeight = useCallback((editorHeight: number) => { if (editorHeight === 0) { setPromptEditorHeight(0); } else { setPromptEditorHeight(editorHeight + PADDING_AND_BORDER); } - }; + }, []); useEffect(() => { const parent = timelineContainerRef.current?.parentElement; @@ -214,6 +223,72 @@ export function ChatBody({ navigator.clipboard?.writeText(content || ''); }; + const handleActionClick: ChatActionClickHandler = (payload) => { + setStickToBottom(true); + switch (payload.type) { + case ChatActionClickType.executeEsqlQuery: + next( + messages.concat({ + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'execute_query', + arguments: JSON.stringify({ + query: payload.query, + }), + trigger: MessageRole.User, + }, + }, + }) + ); + break; + case ChatActionClickType.updateVisualization: + const visualizeQueryMessagesIndex = messages.findIndex( + ({ message }) => message.name === 'visualize_query' + ); + next( + messages.slice(0, visualizeQueryMessagesIndex).concat({ + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'visualize_query', + arguments: JSON.stringify({ + query: payload.query, + userOverrides: payload.userOverrides, + intention: VisualizeESQLUserIntention.visualizeAuto, + }), + trigger: MessageRole.User, + }, + }, + }) + ); + break; + case ChatActionClickType.visualizeEsqlQuery: + next( + messages.concat({ + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.Assistant, + content: '', + function_call: { + name: 'visualize_query', + arguments: JSON.stringify({ + query: payload.query, + intention: VisualizeESQLUserIntention.visualizeAuto, + }), + trigger: MessageRole.User, + }, + }, + }) + ); + break; + } + }; + if (!hasCorrectLicense && !initialConversationId) { footer = ( <> @@ -282,29 +357,8 @@ export function ChatBody({ onStopGenerating={() => { stop(); }} - onActionClick={(payload) => { - setStickToBottom(true); - switch (payload.type) { - case ChatActionClickType.executeEsqlQuery: - next( - messages.concat({ - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.Assistant, - content: '', - function_call: { - name: 'execute_query', - arguments: JSON.stringify({ - query: payload.query, - }), - trigger: MessageRole.User, - }, - }, - }) - ); - break; - } - }} + chatFlyoutSecondSlotHandler={chatFlyoutSecondSlotHandler} + onActionClick={handleActionClick} /> )} @@ -393,7 +447,7 @@ export function ChatBody({ ) : null} - + { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx index 050a17b14a820..83c1496befa4f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_flyout.tsx @@ -4,25 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiLink, EuiPanel, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/css'; +import React, { useCallback, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; -import type { Message } from '../../../common/types'; +import { v4 } from 'uuid'; +import { css } from '@emotion/css'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, useEuiTheme } from '@elastic/eui'; +import { useForceUpdate } from '../../hooks/use_force_update'; import { useCurrentUser } from '../../hooks/use_current_user'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; -import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; import { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { ChatBody } from './chat_body'; +import { ConversationList } from './conversation_list'; +import type { Message } from '../../../common/types'; +import { ChatInlineEditingContent } from './chat_inline_edit'; -const containerClassName = css` - max-height: 100%; -`; +const CONVERSATIONS_SIDEBAR_WIDTH = 260; +const CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED = 34; -const bodyClassName = css` - overflow-y: auto; -`; +const SIDEBAR_WIDTH = 400; export function ChatFlyout({ initialTitle, @@ -43,70 +44,181 @@ export function ChatFlyout({ const connectors = useGenAIConnectors(); - const router = useObservabilityAIAssistantRouter(); - const knowledgeBase = useKnowledgeBase(); const [conversationId, setConversationId] = useState(undefined); - const conversationsHeaderClassName = css` - padding-top: 12px; - padding-bottom: 12px; - border-bottom: solid 1px ${euiTheme.border.color}; + const [expanded, setExpanded] = useState(false); + const [secondSlotContainer, setSecondSlotContainer] = useState(null); + const [isSecondSlotVisible, setIsSecondSlotVisible] = useState(false); + + const sidebarClass = css` + max-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; + min-width: ${expanded ? CONVERSATIONS_SIDEBAR_WIDTH : CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px; + border-right: solid 1px ${euiTheme.border.color}; + `; + + const expandButtonClassName = css` + position: absolute; + margin-top: 16px; + margin-left: ${expanded + ? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED + : 5}px; + padding: ${euiTheme.size.s}; + z-index: 1; `; + const containerClassName = css` + height: 100%; + `; + + const chatBodyContainerClassName = css` + min-width: 0; + `; + + const newChatButtonClassName = css` + position: absolute; + bottom: 31px; + margin-left: ${expanded + ? CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED + : 5}px; + padding: ${euiTheme.size.s}; + z-index: 1; + `; + + const chatBodyKeyRef = useRef(v4()); + const forceUpdate = useForceUpdate(); + const reloadConversation = useCallback(() => { + chatBodyKeyRef.current = v4(); + forceUpdate(); + }, [forceUpdate]); + + const handleClickChat = (id: string) => { + setConversationId(id); + reloadConversation(); + }; + + const handleClickDeleteConversation = () => { + setConversationId(undefined); + reloadConversation(); + }; + + const handleClickNewChat = () => { + if (conversationId) { + setConversationId(undefined); + reloadConversation(); + } + }; + return isOpen ? ( - - - - - {conversationId ? ( - - {i18n.translate('xpack.observabilityAiAssistant.conversationDeepLinkLabel', { - defaultMessage: 'Open conversation', - })} - - ) : ( - - {i18n.translate('xpack.observabilityAiAssistant.conversationListDeepLinkLabel', { - defaultMessage: 'Go to conversations', - })} - + { + onClose(); + setIsSecondSlotVisible(false); + if (secondSlotContainer) { + ReactDOM.unmountComponentAtNode(secondSlotContainer); + } + }} + > + + + + className={expandButtonClassName} + color="text" + data-test-subj="observabilityAiAssistantChatFlyoutButton" + iconType={expanded ? 'transitionLeftIn' : 'transitionLeftOut'} + onClick={() => setExpanded(!expanded)} + /> + + {expanded ? ( + + ) : ( + + )} - + + { setConversationId(conversation.conversation.id); }} + chatFlyoutSecondSlotHandler={{ + container: secondSlotContainer, + setVisibility: setIsSecondSlotVisible, + }} + showLinkToConversationsApp + /> + + + + ) : null; } + +const getFlyoutWidth = ({ + expanded, + isSecondSlotVisible, +}: { + expanded: boolean; + isSecondSlotVisible: boolean; +}) => { + if (!expanded && !isSecondSlotVisible) { + return '40vw'; + } + if (expanded && !isSecondSlotVisible) { + return `calc(40vw + ${CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED}px`; + } + if (!expanded && isSecondSlotVisible) { + return `calc(40vw + ${SIDEBAR_WIDTH}px`; + } + + return `calc(40vw + ${ + CONVERSATIONS_SIDEBAR_WIDTH - CONVERSATIONS_SIDEBAR_WIDTH_COLLAPSED + }px + ${SIDEBAR_WIDTH}px`; +}; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx index 9867958b27d1f..cd4dc0d824590 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_header.tsx @@ -35,6 +35,7 @@ export function ChatHeader({ licenseInvalid, connectors, conversationId, + showLinkToConversationsApp, onCopyConversation, onSaveTitle, }: { @@ -43,6 +44,7 @@ export function ChatHeader({ licenseInvalid: boolean; connectors: UseGenAIConnectorsResult; conversationId?: string; + showLinkToConversationsApp: boolean; onCopyConversation: () => void; onSaveTitle: (title: string) => void; }) { @@ -54,6 +56,11 @@ export function ChatHeader({ setNewTitle(title); }, [title]); + const chatActionsMenuWrapper = css` + position: absolute; + right: 46px; + `; + return ( - + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_inline_edit.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_inline_edit.tsx new file mode 100644 index 0000000000000..862eef7efb5d2 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_inline_edit.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 { EuiFlexGroup } from '@elastic/eui'; +import React, { useRef, useEffect } from 'react'; + +export function ChatInlineEditingContent({ + visible, + setContainer, + style, +}: { + visible?: boolean; + setContainer?: (element: HTMLDivElement | null) => void; + style: React.CSSProperties; +}) { + const containerRef = useRef(null); + + useEffect(() => { + if (containerRef?.current && setContainer) { + setContainer(containerRef.current); + } + }, [setContainer]); + + return ( + + ); +} 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 a07c1f4f55586..48cf4070b0b96 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 @@ -13,7 +13,7 @@ import { omit } from 'lodash'; import type { Feedback } from '../feedback_buttons'; import type { Message } from '../../../common'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import type { ChatActionClickHandler } from './types'; +import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './types'; import type { ObservabilityAIAssistantChatService } from '../../types'; import type { TelemetryEventTypeWithPayload } from '../../analytics'; import { ChatItem } from './chat_item'; @@ -54,6 +54,7 @@ export interface ChatTimelineProps { chatState: ChatState; currentUser?: Pick; startedFrom?: StartedFrom; + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; onEdit: (message: Message, messageAfterEdit: Message) => void; onFeedback: (message: Message, feedback: Feedback) => void; onRegenerate: (message: Message) => void; @@ -68,6 +69,7 @@ export function ChatTimeline({ hasConnector, currentUser, startedFrom, + chatFlyoutSecondSlotHandler, onEdit, onFeedback, onRegenerate, @@ -84,6 +86,8 @@ export function ChatTimeline({ currentUser, startedFrom, chatState, + chatFlyoutSecondSlotHandler, + onActionClick, }); const consolidatedChatItems: Array = []; @@ -106,7 +110,16 @@ export function ChatTimeline({ } return consolidatedChatItems; - }, [chatService, hasConnector, messages, currentUser, startedFrom, chatState]); + }, [ + chatService, + hasConnector, + messages, + currentUser, + startedFrom, + chatState, + chatFlyoutSecondSlotHandler, + onActionClick, + ]); return ( { }; export const ChatHeaderLoading: ComponentStoryObj = { - args: { - loading: true, - }, + args: {}, render: Wrapper, }; export const ChatHeaderError: ComponentStoryObj = { - args: { - error: new Error(), - }, + args: {}, render: Wrapper, }; export const ChatHeaderLoaded: ComponentStoryObj = { args: { - loading: false, selected: '', - conversations: [ - { - id: '', - label: 'New conversation', - }, - { - id: 'first', - label: 'My first conversation', - href: '/my-first-conversation', - }, - { - id: 'second', - label: 'My second conversation', - href: '/my-second-conversation', - }, - ], }, render: Wrapper, }; export const ChatHeaderEmpty: ComponentStoryObj = { args: { - loading: false, selected: '', - conversations: [], }, render: Wrapper, }; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx index 932170c060c61..796a0d4aa275a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/conversation_list.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -21,11 +21,13 @@ import { } from '@elastic/eui'; import { css } from '@emotion/css'; import { i18n } from '@kbn/i18n'; +import { noop } from 'lodash'; +import { useConfirmModal } from '../../hooks/use_confirm_modal'; +import { useKibana } from '../../hooks/use_kibana'; import { NewChatButton } from '../buttons/new_chat_button'; - -const containerClassName = css` - height: 100%; -`; +import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; +import { useAbortableAsync } from '../../hooks/use_abortable_async'; +import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; const titleClassName = css` text-transform: uppercase; @@ -33,6 +35,7 @@ const titleClassName = css` const panelClassName = css` max-height: 100%; + padding-top: 56px; `; const overflowScrollClassName = (scrollBarStyles: string) => css` @@ -46,127 +49,254 @@ const newChatButtonWrapperClassName = css` export function ConversationList({ selected, - loading, - error, - conversations, + onClickChat, onClickNewChat, onClickDeleteConversation, }: { selected: string; - loading: boolean; - error?: any; - conversations?: Array<{ id: string; label: string; href?: string }>; + onClickChat?: (id: string) => void; onClickNewChat: () => void; onClickDeleteConversation: (id: string) => void; }) { + const { + services: { notifications }, + } = useKibana(); + const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); + + const service = useObservabilityAIAssistant(); + + const containerClassName = css` + height: 100%; + border-top: solid 1px ${euiTheme.euiTheme.border.color}; + padding: ${euiTheme.euiTheme.size.s}; + `; + + const { element: confirmDeleteElement, confirm: confirmDeleteFunction } = useConfirmModal({ + title: i18n.translate('xpack.observabilityAiAssistant.flyout.confirmDeleteConversationTitle', { + defaultMessage: 'Delete this conversation?', + }), + children: i18n.translate( + 'xpack.observabilityAiAssistant.flyout.confirmDeleteConversationContent', + { + defaultMessage: 'This action cannot be undone.', + } + ), + confirmButtonText: i18n.translate( + 'xpack.observabilityAiAssistant.flyout.confirmDeleteButtonText', + { + defaultMessage: 'Delete conversation', + } + ), + }); + + const [conversationId, setConversationId] = useState(undefined); + + const [isUpdatingList, setIsUpdatingList] = useState(false); + + const conversations = useAbortableAsync( + ({ signal }) => { + setIsUpdatingList(true); + return service.callApi('POST /internal/observability_ai_assistant/conversations', { + signal, + }); + }, + [service] + ); + + useEffect(() => { + setIsUpdatingList(conversations.loading); + }, [conversations.loading]); + + const displayedConversations = useMemo(() => { + return [ + ...(!conversationId + ? [{ id: '', label: EMPTY_CONVERSATION_TITLE, lastUpdated: '', href: '' }] + : []), + ...(conversations.value?.conversations ?? []).map(({ conversation }) => ({ + id: conversation.id, + label: conversation.title, + lastUpdated: conversation.last_updated, + onClick: () => { + onClickChat?.(conversation.id); + }, + })), + ]; + }, [conversationId, conversations.value?.conversations, onClickChat]); + + const handleDeleteConversation = (id: string) => { + confirmDeleteFunction() + .then(async (confirmed) => { + if (!confirmed) { + return; + } + + setIsUpdatingList(true); + + await service.callApi( + 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', + { + params: { + path: { + conversationId: id, + }, + }, + signal: null, + } + ); + + const isCurrentConversation = id === conversationId; + const hasOtherConversations = conversations.value?.conversations.find( + (conv) => 'id' in conv.conversation && conv.conversation.id !== id + ); + + if (isCurrentConversation) { + setConversationId( + hasOtherConversations ? hasOtherConversations.conversation.id : undefined + ); + } + + conversations.refresh(); + }) + .catch((err) => { + notifications.toasts.addError(err, { + title: i18n.translate( + 'xpack.observabilityAiAssistant.flyout.failedToDeleteConversation', + { + defaultMessage: 'Could not delete conversation', + } + ), + }); + }) + .finally(() => { + setIsUpdatingList(false); + onClickDeleteConversation(id); + }); + }; + + const handleClickNewChat = () => { + onClickNewChat(); + }; + return ( - - - - - - - - - - - - {i18n.translate('xpack.observabilityAiAssistant.conversationList.title', { - defaultMessage: 'Previously', - })} - - - - {loading ? ( - - - - ) : null} - - - - {error ? ( + <> + + + + - - - - + - - {i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.errorMessage', - { - defaultMessage: 'Failed to load', - } - )} + + + + {i18n.translate('xpack.observabilityAiAssistant.conversationList.title', { + defaultMessage: 'Previously', + })} + + {isUpdatingList ? ( + + + + ) : null} - ) : null} - {conversations?.length ? ( - - - {conversations?.map((conversation) => ( - { - onClickDeleteConversation(conversation.id); - }, + + {conversations.error ? ( + + + + + + + + + {i18n.translate( + 'xpack.observabilityAiAssistant.conversationList.errorMessage', + { + defaultMessage: 'Failed to load', } - : undefined + )} + + + + + + ) : null} + + {displayedConversations?.length ? ( + + + {displayedConversations?.map((conversation) => ( + { + onClickChat(conversation.id); + } + : noop + } + extraAction={ + conversation.id + ? { + iconType: 'trash', + 'aria-label': i18n.translate( + 'xpack.observabilityAiAssistant.conversationList.deleteConversationIconLabel', + { + defaultMessage: 'Delete', + } + ), + onClick: () => { + handleDeleteConversation(conversation.id); + }, + } + : undefined + } + /> + ))} + + + ) : null} + + {!isUpdatingList && !conversations.error && !displayedConversations?.length ? ( + + + {i18n.translate( + 'xpack.observabilityAiAssistant.conversationList.noConversations', + { + defaultMessage: 'No conversations', } - /> - ))} - - - ) : null} - - {!loading && !error && !conversations?.length ? ( - - - {i18n.translate( - 'xpack.observabilityAiAssistant.conversationList.noConversations', - { - defaultMessage: 'No conversations', - } - )} - - - ) : null} - - - - - - - - - + )} + + + ) : null} - - - - + + + + + + + + + + + + + + {confirmDeleteElement} + ); } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx index 8ec32e35a66b9..91b058c3f1f3b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/function_list_popover.tsx @@ -158,7 +158,11 @@ function mapFunctions({ selectedFunctionName: string | undefined; }) { return functions - .filter((func) => func.visibility !== FunctionVisibility.System) + .filter( + (func) => + func.visibility !== FunctionVisibility.AssistantOnly && + func.visibility !== FunctionVisibility.Internal + ) .map((func) => ({ label: func.name, searchableLabel: func.descriptionForUser || func.description, diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/types.ts b/x-pack/plugins/observability_ai_assistant/public/components/chat/types.ts index 017f2f81a6f63..4d185150cac2d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/types.ts @@ -4,20 +4,30 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { TypedLensByValueInput } from '@kbn/lens-plugin/public'; type ChatActionClickPayloadBase = { type: TType; } & TExtraProps; type ChatActionClickPayloadExecuteEsql = ChatActionClickPayloadBase< - ChatActionClickType.executeEsqlQuery, - { query: string } + | ChatActionClickType.executeEsqlQuery + | ChatActionClickType.visualizeEsqlQuery + | ChatActionClickType.updateVisualization, + { query: string; userOverrides?: TypedLensByValueInput } >; type ChatActionClickPayload = ChatActionClickPayloadExecuteEsql; export enum ChatActionClickType { executeEsqlQuery = 'executeEsqlQuery', + visualizeEsqlQuery = 'visualizeEsqlQuery', + updateVisualization = 'updateVisualization', } export type ChatActionClickHandler = (payload: ChatActionClickPayload) => void; + +export interface ChatFlyoutSecondSlotHandler { + container?: HTMLDivElement | null; + setVisibility?: (status: boolean) => void; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/esql_code_block.tsx b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/esql_code_block.tsx index a22d0ba28979e..4fe05d323572f 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/message_panel/esql_code_block.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/message_panel/esql_code_block.tsx @@ -65,7 +65,22 @@ export function EsqlCodeBlock({ disabled={actionsDisabled} > {i18n.translate('xpack.observabilityAiAssistant.runThisQuery', { - defaultMessage: 'Run this query', + defaultMessage: 'Display results', + })} + + + + + onActionClick({ type: ChatActionClickType.visualizeEsqlQuery, query: value }) + } + disabled={actionsDisabled} + > + {i18n.translate('xpack.observabilityAiAssistant.visualizeThisQuery', { + defaultMessage: 'Visualize this query', })} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx index 7d2c438daec6e..9110649b4b9b4 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/prompt_editor/prompt_editor.tsx @@ -142,6 +142,8 @@ export function PromptEditor({ useEffect(() => { if (hidden) { onChangeHeight(0); + } else if (containerRef.current) { + onChangeHeight(containerRef.current.clientHeight); } }, [hidden, onChangeHeight]); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx b/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx index aed661f27a220..91547deed0021 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/render_function.tsx @@ -7,15 +7,27 @@ import React from 'react'; import { Message } from '../../common'; import { useObservabilityAIAssistantChatService } from '../hooks/use_observability_ai_assistant_chat_service'; +import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './chat/types'; interface Props { name: string; arguments: string | undefined; response: Message['message']; + onActionClick: ChatActionClickHandler; + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; } export function RenderFunction(props: Props) { const chatService = useObservabilityAIAssistantChatService(); - - return <>{chatService.renderFunction(props.name, props.arguments, props.response)}; + return ( + <> + {chatService.renderFunction( + props.name, + props.arguments, + props.response, + props.onActionClick, + props.chatFlyoutSecondSlotHandler + )} + + ); } 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 4bdbb31b15dcc..056744fa101ae 100644 --- a/x-pack/plugins/observability_ai_assistant/public/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/public/functions/index.ts @@ -11,6 +11,7 @@ import type { RegisterRenderFunctionDefinition, } from '../types'; import { registerLensRenderFunction } from './lens'; +import { registerVisualizeQueryRenderFunction } from './visualize_esql'; export async function registerFunctions({ registerRenderFunction, @@ -22,4 +23,5 @@ export async function registerFunctions({ pluginsStart: ObservabilityAIAssistantPluginStartDependencies; }) { registerLensRenderFunction({ service, pluginsStart, registerRenderFunction }); + registerVisualizeQueryRenderFunction({ service, pluginsStart, registerRenderFunction }); } diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx new file mode 100644 index 0000000000000..789697fbaaaa8 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.test.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; +import type { LensPublicStart } from '@kbn/lens-plugin/public'; +import { lensPluginMock } from '@kbn/lens-plugin/public/mocks/lens_plugin_mock'; +import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; +import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; +import { VisualizeESQL } from './visualize_esql'; + +describe('VisualizeESQL', () => { + function renderComponent( + userOverrides?: unknown, + newLensService?: LensPublicStart, + setVisibilitySpy?: () => void + ) { + const lensService = newLensService ?? lensPluginMock.createStartContract(); + const dataViewsService = { + ...dataViewPluginMocks.createStartContract(), + create: jest.fn().mockReturnValue( + Promise.resolve({ + title: 'foo', + id: 'foo', + toSpec: jest.fn(), + isPersisted: jest.fn().mockReturnValue(false), + }) + ), + }; + const uiActionsService = uiActionsPluginMock.createStartContract(); + const columns = [ + { + name: 'bytes', + id: 'bytes', + meta: { + type: 'number', + }, + }, + { + name: 'destination', + id: 'destination', + meta: { + type: 'keyword', + }, + }, + ] as DatatableColumn[]; + render( + + ); + } + + it('should render the embeddable if no initial input is given', async () => { + renderComponent(); + await waitFor(() => + expect(screen.getByTestId('observabilityAiAssistantESQLLensChart')).toMatchInlineSnapshot(` +
+ + Lens Embeddable Component + +
+ `) + ); + }); + + it('should run the suggestions api if no initial input is given', async () => { + const suggestionsApiSpy = jest.fn(); + const lensService = { + ...lensPluginMock.createStartContract(), + stateHelperApi: jest.fn().mockResolvedValue({ + formula: jest.fn(), + suggestions: suggestionsApiSpy, + }), + }; + renderComponent(undefined, lensService); + await waitFor(() => expect(suggestionsApiSpy).toHaveBeenCalled()); + }); + + it('should not run the suggestions api if no initial input is given', async () => { + const suggestionsApiSpy = jest.fn(); + const lensService = { + ...lensPluginMock.createStartContract(), + stateHelperApi: jest.fn().mockResolvedValue({ + formula: jest.fn(), + suggestions: suggestionsApiSpy, + }), + }; + renderComponent({}, lensService); + await waitFor(() => expect(suggestionsApiSpy).not.toHaveBeenCalled()); + }); + + it('should run the setVisibility callback if edit button is clicked', async () => { + const setVisibilitySpy = jest.fn(); + renderComponent({}, undefined, setVisibilitySpy); + await waitFor(() => { + userEvent.click(screen.getByTestId('observabilityAiAssistantLensESQLEditButton')); + expect(setVisibilitySpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx new file mode 100644 index 0000000000000..05145c6130b4f --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/functions/visualize_esql.tsx @@ -0,0 +1,379 @@ +/* + * 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 as uuidv4 } from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiToolTip, + EuiButtonIcon, +} from '@elastic/eui'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { DatatableColumn } from '@kbn/expressions-plugin/common'; +import { getLensAttributesFromSuggestion } from '@kbn/visualization-utils'; +import type { + LensPublicStart, + TypedLensByValueInput, + InlineEditLensEmbeddableContext, +} from '@kbn/lens-plugin/public'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import ReactDOM from 'react-dom'; +import useAsync from 'react-use/lib/useAsync'; +import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; +import { + VisualizeESQLFunctionArguments, + VisualizeESQLUserIntention, +} from '../../common/functions/visualize_esql'; +import type { + ObservabilityAIAssistantPluginStartDependencies, + ObservabilityAIAssistantService, + RegisterRenderFunctionDefinition, + RenderFunction, +} from '../types'; +import { + type ChatActionClickHandler, + ChatActionClickType, + ChatFlyoutSecondSlotHandler, +} from '../components/chat/types'; + +interface VisualizeLensResponse { + content: DatatableColumn[]; +} + +interface VisualizeESQLProps { + /** Lens start contract, get the ES|QL charts suggestions api */ + lens: LensPublicStart; + /** Dataviews start contract, creates an adhoc dataview */ + dataViews: DataViewsServicePublic; + /** UiActions start contract, triggers the inline editing flyout */ + uiActions: UiActionsStart; + /** Datatable columns as returned from the ES|QL _query api, slightly processed to be kibana compliant */ + columns: DatatableColumn[]; + /** The ES|QL query */ + query: string; + /** Actions handler */ + onActionClick: ChatActionClickHandler; + /** Optional, overwritten ES|QL Lens chart attributes + * If not given, the embeddable gets them from the suggestions api + */ + userOverrides?: unknown; + /** Optional, should be passed if the embeddable is rendered in a flyout + * If not given, the inline editing push flyout won't open + * The code will be significantly improved, + * if this is addressed https://github.com/elastic/eui/issues/7443 + */ + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; + /** User's preferation chart type as it comes from the model */ + preferredChartType?: string; +} + +function generateId() { + return uuidv4(); +} + +export function VisualizeESQL({ + lens, + dataViews, + uiActions, + columns, + query, + onActionClick, + userOverrides, + chatFlyoutSecondSlotHandler, + preferredChartType, +}: VisualizeESQLProps) { + // fetch the pattern from the query + const indexPattern = getIndexPatternFromESQLQuery(query); + const lensHelpersAsync = useAsync(() => { + return lens.stateHelperApi(); + }, [lens]); + + const dataViewAsync = useAsync(() => { + return dataViews.create({ + title: indexPattern, + }); + }, [indexPattern]); + + const [isSaveModalOpen, setIsSaveModalOpen] = useState(false); + const [lensInput, setLensInput] = useState( + userOverrides as TypedLensByValueInput + ); + const [lensLoadEvent, setLensLoadEvent] = useState< + InlineEditLensEmbeddableContext['lensEvent'] | null + >(null); + + const onLoad = useCallback( + ( + isLoading: boolean, + adapters: InlineEditLensEmbeddableContext['lensEvent']['adapters'] | undefined, + lensEmbeddableOutput$?: InlineEditLensEmbeddableContext['lensEvent']['embeddableOutput$'] + ) => { + const adapterTables = adapters?.tables?.tables; + if (adapterTables && !isLoading) { + setLensLoadEvent({ + adapters, + embeddableOutput$: lensEmbeddableOutput$, + }); + } + }, + [] + ); + + // initialization + useEffect(() => { + if (lensHelpersAsync.value && dataViewAsync.value && !lensInput) { + const context = { + dataViewSpec: dataViewAsync.value?.toSpec(), + fieldName: '', + textBasedColumns: columns, + query: { + esql: query, + }, + }; + + const chartSuggestions = lensHelpersAsync.value.suggestions(context, dataViewAsync.value); + if (chartSuggestions?.length) { + let [suggestion] = chartSuggestions; + + if (chartSuggestions.length > 1 && preferredChartType) { + const suggestionFromModel = chartSuggestions.find( + (s) => + s.title.includes(preferredChartType) || s.visualizationId.includes(preferredChartType) + ); + if (suggestionFromModel) { + suggestion = suggestionFromModel; + } + } + + const attrs = getLensAttributesFromSuggestion({ + filters: [], + query: { + esql: query, + }, + suggestion, + dataView: dataViewAsync.value, + }) as TypedLensByValueInput['attributes']; + + const lensEmbeddableInput = { + attributes: attrs, + id: generateId(), + }; + setLensInput(lensEmbeddableInput); + } + } + }, [columns, dataViewAsync.value, lensHelpersAsync.value, lensInput, query, preferredChartType]); + + // trigger options to open the inline editing flyout correctly + const triggerOptions: InlineEditLensEmbeddableContext | undefined = useMemo(() => { + if (lensLoadEvent && lensInput?.attributes) { + return { + attributes: lensInput?.attributes, + lensEvent: lensLoadEvent, + onUpdate: (newAttributes: TypedLensByValueInput['attributes']) => { + if (lensInput) { + const newInput = { + ...lensInput, + attributes: newAttributes, + }; + setLensInput(newInput); + } + }, + onApply: (newAttributes: TypedLensByValueInput['attributes']) => { + const newInput = { + ...lensInput, + attributes: newAttributes, + }; + onActionClick({ + type: ChatActionClickType.updateVisualization, + userOverrides: newInput, + query, + }); + chatFlyoutSecondSlotHandler?.setVisibility?.(false); + if (chatFlyoutSecondSlotHandler?.container) { + ReactDOM.unmountComponentAtNode(chatFlyoutSecondSlotHandler.container); + } + }, + onCancel: () => { + onActionClick({ + type: ChatActionClickType.updateVisualization, + userOverrides: lensInput, + query, + }); + chatFlyoutSecondSlotHandler?.setVisibility?.(false); + if (chatFlyoutSecondSlotHandler?.container) { + ReactDOM.unmountComponentAtNode(chatFlyoutSecondSlotHandler.container); + } + }, + container: chatFlyoutSecondSlotHandler?.container, + }; + } + }, [chatFlyoutSecondSlotHandler, lensInput, lensLoadEvent, onActionClick, query]); + + if (!lensHelpersAsync.value || !dataViewAsync.value || !lensInput) { + return ; + } + + return ( + <> + + + + + { + chatFlyoutSecondSlotHandler?.setVisibility?.(true); + if (triggerOptions) { + uiActions.getTrigger('IN_APP_EMBEDDABLE_EDIT_TRIGGER').exec(triggerOptions); + } + }} + data-test-subj="observabilityAiAssistantLensESQLEditButton" + aria-label={i18n.translate('xpack.observabilityAiAssistant.lensESQLFunction.edit', { + defaultMessage: 'Edit visualization', + })} + /> + + + + setIsSaveModalOpen(true)} + data-test-subj="observabilityAiAssistantLensESQLSaveButton" + aria-label={i18n.translate( + 'xpack.observabilityAiAssistant.lensESQLFunction.save', + { + defaultMessage: 'Save visualization', + } + )} + /> + + + + + + + + + {isSaveModalOpen ? ( + { + setIsSaveModalOpen(() => false); + }} + // For now, we don't want to allow saving ESQL charts to the library + isSaveable={false} + /> + ) : null} + + ); +} + +enum ChartType { + XY = 'XY', + Bar = 'Bar', + Line = 'Line', + Donut = 'Donut', + Heatmap = 'Heat map', + Treemap = 'Treemap', + Tagcloud = 'Tag cloud', + Waffle = 'Waffle', +} + +export function registerVisualizeQueryRenderFunction({ + service, + registerRenderFunction, + pluginsStart, +}: { + service: ObservabilityAIAssistantService; + registerRenderFunction: RegisterRenderFunctionDefinition; + pluginsStart: ObservabilityAIAssistantPluginStartDependencies; +}) { + registerRenderFunction( + 'visualize_query', + ({ + arguments: { query, userOverrides, intention }, + response, + onActionClick, + chatFlyoutSecondSlotHandler, + }: Parameters>[0]) => { + const { content } = response as VisualizeLensResponse; + + let preferredChartType: string | undefined; + + switch (intention) { + case VisualizeESQLUserIntention.executeAndReturnResults: + case VisualizeESQLUserIntention.generateQueryOnly: + case VisualizeESQLUserIntention.visualizeAuto: + break; + + case VisualizeESQLUserIntention.visualizeBar: + preferredChartType = ChartType.Bar; + break; + + case VisualizeESQLUserIntention.visualizeDonut: + preferredChartType = ChartType.Donut; + break; + + case VisualizeESQLUserIntention.visualizeHeatmap: + preferredChartType = ChartType.Heatmap; + break; + + case VisualizeESQLUserIntention.visualizeLine: + preferredChartType = ChartType.Line; + break; + + case VisualizeESQLUserIntention.visualizeTagcloud: + preferredChartType = ChartType.Tagcloud; + break; + + case VisualizeESQLUserIntention.visualizeTreemap: + preferredChartType = ChartType.Treemap; + break; + + case VisualizeESQLUserIntention.visualizeWaffle: + preferredChartType = ChartType.Waffle; + break; + + case VisualizeESQLUserIntention.visualizeXy: + preferredChartType = ChartType.XY; + break; + } + + return ( + + ); + } + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx index 4505632245191..4620c0cf2775d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/routes/conversations/conversation_view.tsx @@ -4,39 +4,35 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { v4 } from 'uuid'; import { css } from '@emotion/css'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { euiThemeVars } from '@kbn/ui-theme'; -import React, { useMemo, useRef, useState } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; -import { v4 } from 'uuid'; import { ChatBody } from '../../components/chat/chat_body'; import { ConversationList } from '../../components/chat/conversation_list'; import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider'; import { useAbortableAsync } from '../../hooks/use_abortable_async'; -import { useConfirmModal } from '../../hooks/use_confirm_modal'; import { useCurrentUser } from '../../hooks/use_current_user'; import { useForceUpdate } from '../../hooks/use_force_update'; import { useGenAIConnectors } from '../../hooks/use_genai_connectors'; -import { useKibana } from '../../hooks/use_kibana'; import { useKnowledgeBase } from '../../hooks/use_knowledge_base'; import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; import { useObservabilityAIAssistantParams } from '../../hooks/use_observability_ai_assistant_params'; import { useObservabilityAIAssistantRouter } from '../../hooks/use_observability_ai_assistant_router'; -import { EMPTY_CONVERSATION_TITLE } from '../../i18n'; +import { ChatInlineEditingContent } from '../../components/chat/chat_inline_edit'; const containerClassName = css` max-width: 100%; `; -const conversationListContainerName = css` - min-width: 250px; - width: 250px; - border-right: solid 1px ${euiThemeVars.euiColorLightShade}; -`; +const SECOND_SLOT_CONTAINER_WIDTH = 400; export function ConversationView() { + const { euiTheme } = useEuiTheme(); + const currentUser = useCurrentUser(); const service = useObservabilityAIAssistant(); @@ -49,24 +45,6 @@ export function ConversationView() { const { path } = useObservabilityAIAssistantParams('/conversations/*'); - const { - services: { notifications }, - } = useKibana(); - - const { element: confirmDeleteElement, confirm: confirmDeleteFunction } = useConfirmModal({ - title: i18n.translate('xpack.observabilityAiAssistant.confirmDeleteConversationTitle', { - defaultMessage: 'Delete this conversation?', - }), - children: i18n.translate('xpack.observabilityAiAssistant.confirmDeleteConversationContent', { - defaultMessage: 'This action cannot be undone.', - }), - confirmButtonText: i18n.translate('xpack.observabilityAiAssistant.confirmDeleteButtonText', { - defaultMessage: 'Delete conversation', - }), - }); - - const [isUpdatingList, setIsUpdatingList] = useState(false); - const chatService = useAbortableAsync( ({ signal }) => { return service.start({ signal }); @@ -87,6 +65,9 @@ export function ConversationView() { const keepPreviousKeyRef = useRef(false); const prevConversationId = usePrevious(conversationId); + const [secondSlotContainer, setSecondSlotContainer] = useState(null); + const [isSecondSlotVisible, setIsSecondSlotVisible] = useState(false); + if (conversationId !== prevConversationId && keepPreviousKeyRef.current === false) { chatBodyKeyRef.current = v4(); } @@ -104,21 +85,6 @@ export function ConversationView() { [service] ); - const displayedConversations = useMemo(() => { - return [ - ...(!conversationId ? [{ id: '', label: EMPTY_CONVERSATION_TITLE }] : []), - ...(conversations.value?.conversations ?? []).map((conv) => ({ - id: conv.conversation.id, - label: conv.conversation.title, - href: observabilityAIAssistantRouter.link('/conversations/{conversationId}', { - path: { - conversationId: conv.conversation.id, - }, - }), - })), - ]; - }, [conversations.value?.conversations, conversationId, observabilityAIAssistantRouter]); - function navigateToConversation(nextConversationId?: string, usePrevConversationKey?: boolean) { if (nextConversationId) { observabilityAIAssistantRouter.push('/conversations/{conversationId}', { @@ -136,16 +102,59 @@ export function ConversationView() { conversations.refresh(); } + const handleConversationUpdate = (conversation: { conversation: { id: string } }) => { + if (!conversationId) { + keepPreviousKeyRef.current = true; + navigateToConversation(conversation.conversation.id); + } + handleRefreshConversations(); + }; + + useEffect(() => { + return () => { + setIsSecondSlotVisible(false); + if (secondSlotContainer) { + ReactDOM.unmountComponentAtNode(secondSlotContainer); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const conversationListContainerName = css` + min-width: 250px; + width: 250px; + border-right: solid 1px ${euiThemeVars.euiColorLightShade}; + `; + + const sidebarContainerClass = css` + display: flex; + position: absolute; + z-index: 1; + top: 56px; + right: 0; + height: calc(100% - 56px); + background-color: ${euiTheme.colors.lightestShade}; + width: ${isSecondSlotVisible ? SECOND_SLOT_CONTAINER_WIDTH : 0}px; + border-top: solid 1px ${euiThemeVars.euiColorLightShade}; + border-left: solid 1px ${euiThemeVars.euiColorLightShade}; + + .euiFlyoutHeader { + padding: ${euiTheme.size.m}; + } + + .euiFlyoutFooter { + padding: ${euiTheme.size.m}; + padding-top: ${euiTheme.size.l}; + padding-bottom: ${euiTheme.size.l}; + } + `; + return ( <> - {confirmDeleteElement} { if (conversationId) { observabilityAIAssistantRouter.push('/conversations/new', { @@ -158,55 +167,13 @@ export function ConversationView() { forceUpdate(); } }} + onClickChat={(id) => { + navigateToConversation(id, false); + }} onClickDeleteConversation={(id) => { - confirmDeleteFunction() - .then(async (confirmed) => { - if (!confirmed) { - return; - } - - setIsUpdatingList(true); - - await service.callApi( - 'DELETE /internal/observability_ai_assistant/conversation/{conversationId}', - { - params: { - path: { - conversationId: id, - }, - }, - signal: null, - } - ); - - const isCurrentConversation = id === conversationId; - const hasOtherConversations = conversations.value?.conversations.find( - (conv) => 'id' in conv.conversation && conv.conversation.id !== id - ); - - if (isCurrentConversation) { - navigateToConversation( - hasOtherConversations - ? conversations.value!.conversations[0].conversation.id - : undefined - ); - } - - conversations.refresh(); - }) - .catch((error) => { - notifications.toasts.addError(error, { - title: i18n.translate( - 'xpack.observabilityAiAssistant.failedToDeleteConversation', - { - defaultMessage: 'Could not delete conversation', - } - ), - }); - }) - .finally(() => { - setIsUpdatingList(false); - }); + if (conversationId === id) { + navigateToConversation(undefined, false); + } }} /> @@ -220,6 +187,7 @@ export function ConversationView() { ) : null} + {chatService.value && ( { - if (!conversationId) { - keepPreviousKeyRef.current = true; - navigateToConversation(conversation.conversation.id); - } - handleRefreshConversations(); + onConversationUpdate={handleConversationUpdate} + chatFlyoutSecondSlotHandler={{ + container: secondSlotContainer, + setVisibility: setIsSecondSlotVisible, }} /> + +
+ +
)} diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts index 1a001e7b10b1a..211f25b045b77 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts @@ -116,7 +116,7 @@ export async function createChatService({ return { analytics, - renderFunction: (name, args, response) => { + renderFunction: (name, args, response, onActionClick, chatFlyoutSecondSlotHandler) => { const fn = renderFunctionRegistry.get(name); if (!fn) { @@ -130,7 +130,12 @@ export async function createChatService({ data: JSON.parse(response.data ?? '{}'), }; - return fn?.({ response: parsedResponse, arguments: parsedArguments }); + return fn?.({ + response: parsedResponse, + arguments: parsedArguments, + onActionClick, + chatFlyoutSecondSlotHandler, + }); }, getContexts: () => contextDefinitions, getFunctions, @@ -191,7 +196,13 @@ export async function createChatService({ return new Observable((subscriber) => { const contexts = ['core', 'apm']; - const functions = getFunctions({ contexts }); + const functions = getFunctions({ contexts }).filter((fn) => { + const visibility = fn.visibility ?? FunctionVisibility.All; + + return ( + visibility === FunctionVisibility.All || visibility === FunctionVisibility.AssistantOnly + ); + }); client('POST /internal/observability_ai_assistant/chat', { params: { @@ -202,9 +213,7 @@ export async function createChatService({ functions: callFunctions === 'none' ? [] - : functions - .filter((fn) => fn.visibility !== FunctionVisibility.User) - .map((fn) => pick(fn, 'name', 'description', 'parameters')), + : functions.map((fn) => pick(fn, 'name', 'description', 'parameters')), }, }, signal, diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index ce2ea829081db..418c7eca16b19 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -6,11 +6,12 @@ */ import type { AnalyticsServiceStart } from '@kbn/core/public'; +import type { FeaturesPluginStart, FeaturesPluginSetup } from '@kbn/features-plugin/public'; +import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { DataViewsPublicPluginSetup, DataViewsPublicPluginStart, } from '@kbn/data-views-plugin/public'; -import type { FeaturesPluginSetup, FeaturesPluginStart } from '@kbn/features-plugin/public'; import type { LensPublicSetup, LensPublicStart } from '@kbn/lens-plugin/public'; import type { ILicense, LicensingPluginStart } from '@kbn/licensing-plugin/public'; import { MlPluginSetup, MlPluginStart } from '@kbn/ml-plugin/public'; @@ -39,6 +40,7 @@ import type { Message, PendingMessage, } from '../common/types'; +import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './components/chat/types'; import type { ObservabilityAIAssistantAPIClient } from './api'; import type { InsightProps } from './components/insight/insight'; import type { UseGenAIConnectorsResult } from './hooks/use_genai_connectors'; @@ -73,7 +75,9 @@ export interface ObservabilityAIAssistantChatService { renderFunction: ( name: string, args: string | undefined, - response: { data?: string; content?: string } + response: { data?: string; content?: string }, + onActionClick: ChatActionClickHandler, + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler ) => React.ReactNode; } @@ -90,6 +94,8 @@ export interface ObservabilityAIAssistantService { export type RenderFunction = (options: { arguments: TArguments; response: TResponse; + onActionClick: ChatActionClickHandler; + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; }) => React.ReactNode; export type RegisterRenderFunctionDefinition< @@ -122,6 +128,7 @@ export interface ObservabilityAIAssistantPluginStartDependencies { security: SecurityPluginStart; share: SharePluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; + uiActions: UiActionsStart; ml: MlPluginStart; } diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx index c69f4994c0539..600256d66a7bc 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx @@ -25,6 +25,7 @@ describe('getTimelineItemsFromConversation', () => { hasConnector: true, messages: [], chatState: ChatState.Ready, + onActionClick: jest.fn(), }); expect(items.length).toBe(1); @@ -57,6 +58,7 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), }); }); it('excludes the system message', () => { @@ -129,6 +131,7 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), }); }); @@ -227,6 +230,11 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), + chatFlyoutSecondSlotHandler: { + container: null, + setVisibility: jest.fn(), + }, }); }); @@ -261,7 +269,9 @@ describe('getTimelineItemsFromConversation', () => { expect(mockChatService.renderFunction).toHaveBeenCalledWith( 'my_render_function', JSON.stringify({ foo: 'bar' }), - { content: '[]', name: 'my_render_function', role: 'user' } + { content: '[]', name: 'my_render_function', role: 'user' }, + expect.any(Function), + { container: null, setVisibility: expect.any(Function) } ); expect(container.textContent).toEqual('Rendered'); @@ -313,6 +323,7 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), }); }); @@ -384,6 +395,7 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), }); }); @@ -425,6 +437,7 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), }); }); @@ -475,6 +488,7 @@ describe('getTimelineItemsFromConversation', () => { }, }, ], + onActionClick: jest.fn(), }); }); @@ -532,6 +546,7 @@ describe('getTimelineItemsFromConversation', () => { }, ...extraMessages, ], + onActionClick: jest.fn(), }); }; diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx index 2b910211ba5fd..40b54708e5b6c 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx @@ -17,6 +17,7 @@ import { RenderFunction } from '../components/render_function'; import type { ObservabilityAIAssistantChatService } from '../types'; import { ChatState } from '../hooks/use_chat'; import { safeJsonParse } from './safe_json_parse'; +import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from '../components/chat/types'; function convertMessageToMarkdownCodeBlock(message: Message['message']) { let value: object; @@ -64,6 +65,8 @@ export function getTimelineItemsfromConversation({ messages, startedFrom, chatState, + chatFlyoutSecondSlotHandler, + onActionClick, }: { chatService: ObservabilityAIAssistantChatService; currentUser?: Pick; @@ -71,6 +74,8 @@ export function getTimelineItemsfromConversation({ messages: Message[]; startedFrom?: StartedFrom; chatState: ChatState; + chatFlyoutSecondSlotHandler?: ChatFlyoutSecondSlotHandler; + onActionClick: ChatActionClickHandler; }): ChatTimelineItem[] { const messagesWithoutSystem = messages.filter( (message) => message.message.role !== MessageRole.System @@ -163,6 +168,8 @@ export function getTimelineItemsfromConversation({ name={message.message.name} arguments={prevFunctionCall?.arguments} response={message.message} + onActionClick={onActionClick} + chatFlyoutSecondSlotHandler={chatFlyoutSecondSlotHandler} /> ) : undefined; diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/get_dataset_info.ts b/x-pack/plugins/observability_ai_assistant/server/functions/get_dataset_info.ts index 4969ddfb7e402..f1e95c37fdb89 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/get_dataset_info.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/get_dataset_info.ts @@ -20,7 +20,7 @@ export function registerGetDatasetInfoFunction({ { name: 'get_dataset_info', contexts: ['core'], - visibility: FunctionVisibility.System, + visibility: FunctionVisibility.AssistantOnly, description: `Use this function to get information about indices/datasets available and the fields available on them. providing empty string as index name will retrieve all indices diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts index 12075a56942f6..d02f943c3523e 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts @@ -11,10 +11,11 @@ import { registerSummarizationFunction } from './summarize'; import { ChatRegistrationFunction } from '../service/types'; import { registerAlertsFunction } from './alerts'; import { registerElasticsearchFunction } from './elasticsearch'; -import { registerEsqlFunction } from './esql'; +import { registerQueryFunction } from './query'; import { registerGetDatasetInfoFunction } from './get_dataset_info'; import { registerLensFunction } from './lens'; import { registerKibanaFunction } from './kibana'; +import { registerVisualizeESQLFunction } from './visualize_esql'; export type FunctionRegistrationParameters = Omit< Parameters[0], @@ -50,19 +51,22 @@ export const registerFunctions: ChatRegistrationFunction = async ({ You can use Github-flavored Markdown in your responses. If a function returns an array, consider using a Markdown table to format the response. - If multiple functions are suitable, use the most specific and easy one. E.g., when the user asks to visualise APM data, use the APM functions (if available) rather than Lens. + If multiple functions are suitable, use the most specific and easy one. E.g., when the user asks to visualise APM data, use the APM functions (if available) rather than "query". - If a function call fails, DO NOT UNDER ANY CIRCUMSTANCES execute it again. Ask the user for guidance and offer them options. + Use the "get_dataset_info" function if it is not clear what fields or indices the user means, or if you want to get more information about the mappings. Note that ES|QL (the Elasticsearch query language, which is NOT Elasticsearch SQL, but a new piped language) is the preferred query language. - Use the "get_dataset_info" function if it is not clear what fields or indices the user means, or if you want to get more information about the mappings. - - If the user asks about a query, or ES|QL, always call the "esql" function. DO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries or explain anything about the ES|QL query language yourself. - Even if the "recall" function was used before that, follow it up with the "esql" function. If a query fails, do not attempt to correct it yourself. Again you should call the "esql" function, + If the user wants to visualize data, or run any arbitrary query, always use the "query" function. DO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries + or explain anything about the ES|QL query language yourself. + + Even if the "recall" function was used before that, follow it up with the "query" function. If a query fails, do not attempt to correct it yourself. Again you should call the "query" function, even if it has been called before. - If the "get_dataset_info" function returns no data, and the user asks for a query, generate a query anyway with the "esql" function, but be explicit about it potentially being incorrect. + When the "visualize_query" function has been called, a visualization has been displayed to the user. DO NOT UNDER ANY CIRCUMSTANCES follow up a "visualize_query" function call with your own visualization attempt. + If the "execute_query" function has been called, summarize these results for the user. The user does not see a visualization in this case. + + If the "get_dataset_info" function returns no data, and the user asks for a query, generate a query anyway with the "query" function, but be explicit about it potentially being incorrect. ` ); @@ -76,13 +80,14 @@ export const registerFunctions: ChatRegistrationFunction = async ({ registerSummarizationFunction(registrationParameters); registerRecallFunction(registrationParameters); registerLensFunction(registrationParameters); + registerVisualizeESQLFunction(registrationParameters); } else { description += `You do not have a working memory. Don't try to recall information via the "recall" function. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base. A banner is available at the top of the conversation to set this up.`; } registerElasticsearchFunction(registrationParameters); registerKibanaFunction(registrationParameters); - registerEsqlFunction(registrationParameters); + registerQueryFunction(registrationParameters); registerAlertsFunction(registrationParameters); registerGetDatasetInfoFunction(registrationParameters); diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/correct_common_esql_mistakes.test.ts b/x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.test.ts similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/correct_common_esql_mistakes.test.ts rename to x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.test.ts diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/correct_common_esql_mistakes.ts b/x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.ts similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/correct_common_esql_mistakes.ts rename to x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.ts diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-abs.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-abs.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-abs.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-abs.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-acos.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-acos.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-acos.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-acos.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-asin.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-asin.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-asin.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-asin.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-atan.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-atan.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan2.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-atan2.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-atan2.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-atan2.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-auto_bucket.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-auto_bucket.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-auto_bucket.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-auto_bucket.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-avg.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-avg.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-avg.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-avg.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-case.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-case.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-case.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-case.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ceil.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-ceil.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ceil.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-ceil.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-coalesce.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-coalesce.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-coalesce.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-coalesce.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-concat.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-concat.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-concat.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-concat.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cos.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-cos.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cos.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-cos.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cosh.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-cosh.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-cosh.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-cosh.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-count.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-count.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count_distinct.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-count_distinct.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-count_distinct.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-count_distinct.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_extract.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_extract.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_extract.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_extract.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_format.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_format.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_format.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_format.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_parse.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_parse.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_parse.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_parse.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_trunc.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_trunc.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-date_trunc.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-date_trunc.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-dissect.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-dissect.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-dissect.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-dissect.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-drop.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-drop.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-drop.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-drop.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-e.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-e.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-e.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-e.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-enrich.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-enrich.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-enrich.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-enrich.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-eval.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-eval.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-eval.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-eval.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-floor.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-floor.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-floor.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-floor.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-from.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-from.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-from.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-from.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-greatest.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-greatest.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-greatest.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-greatest.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-grok.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-grok.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-grok.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-grok.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-keep.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-keep.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-keep.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-keep.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-least.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-least.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-least.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-least.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-left.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-left.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-left.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-left.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-length.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-length.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-length.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-length.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limit.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-limit.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limit.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-limit.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limitations.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-limitations.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-limitations.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-limitations.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-log10.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-log10.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-log10.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-log10.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ltrim.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-ltrim.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-ltrim.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-ltrim.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-max.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-max.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-max.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-max.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-median.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-median.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median_absolute_deviation.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-median_absolute_deviation.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-median_absolute_deviation.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-median_absolute_deviation.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-min.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-min.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-min.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-min.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_avg.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_avg.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_avg.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_avg.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_concat.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_concat.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_concat.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_concat.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_count.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_count.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_count.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_count.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_dedupe.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_dedupe.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_dedupe.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_dedupe.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_expand.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_expand.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_expand.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_expand.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_max.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_max.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_max.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_max.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_median.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_median.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_median.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_median.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_min.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_min.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_min.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_min.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_sum.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_sum.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-mv_sum.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-mv_sum.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-now.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-now.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-now.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-now.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-operators.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-operators.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-operators.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-operators.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-overview.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-overview.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-overview.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-overview.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-percentile.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-percentile.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-percentile.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-percentile.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pi.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-pi.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pi.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-pi.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pow.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-pow.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-pow.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-pow.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-processing-commands.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-processing-commands.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-processing-commands.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-processing-commands.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rename.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-rename.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rename.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-rename.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-replace.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-replace.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-replace.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-replace.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-right.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-right.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-right.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-right.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-round.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-round.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-round.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-round.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-row.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-row.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-row.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-row.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rtrim.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-rtrim.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-rtrim.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-rtrim.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-show.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-show.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-show.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-show.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sin.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sin.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sin.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sin.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sinh.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sinh.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sinh.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sinh.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sort.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sort.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sort.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sort.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-source-commands.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-source-commands.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-source-commands.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-source-commands.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-split.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-split.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-split.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-split.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sqrt.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sqrt.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sqrt.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sqrt.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-stats.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-stats.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-stats.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-stats.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-substring.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-substring.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-substring.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-substring.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sum.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sum.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-sum.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-sum.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-syntax.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-syntax.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-syntax.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-syntax.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tan.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-tan.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tan.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-tan.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tanh.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-tanh.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tanh.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-tanh.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tau.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-tau.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-tau.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-tau.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_boolean.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_boolean.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_boolean.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_boolean.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_cartesianpoint.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_cartesianpoint.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_cartesianpoint.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_cartesianpoint.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_datetime.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_datetime.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_datetime.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_datetime.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_degrees.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_degrees.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_degrees.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_degrees.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_double.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_double.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_double.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_double.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_geopoint.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_geopoint.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_geopoint.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_geopoint.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_integer.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_integer.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_integer.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_integer.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_ip.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_ip.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_ip.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_ip.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_long.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_long.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_long.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_long.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_radians.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_radians.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_radians.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_radians.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_string.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_string.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_string.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_string.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_unsigned_long.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_unsigned_long.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_unsigned_long.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_unsigned_long.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_version.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_version.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-to_version.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-to_version.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-trim.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-trim.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-trim.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-trim.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-where.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-where.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/esql_docs/esql-where.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/esql_docs/esql-where.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts similarity index 68% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts rename to x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts index a8f3ad22ebe5b..b69188d81b84a 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/esql/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts @@ -13,6 +13,10 @@ import { lastValueFrom, type Observable } from 'rxjs'; import { promisify } from 'util'; import type { FunctionRegistrationParameters } from '..'; import type { ChatCompletionChunkEvent } from '../../../common/conversation_complete'; +import { + VisualizeESQLUserIntention, + VISUALIZE_ESQL_USER_INTENTIONS, +} from '../../../common/functions/visualize_esql'; import { FunctionVisibility, MessageRole } from '../../../common/types'; import { concatenateChatCompletionChunks } from '../../../common/utils/concatenate_chat_completion_chunks'; import { emitWithConcatenatedMessage } from '../../../common/utils/emit_with_concatenated_message'; @@ -59,7 +63,7 @@ const loadEsqlDocs = once(async () => { ); }); -export function registerEsqlFunction({ +export function registerQueryFunction({ client, registerFunction, resources, @@ -68,8 +72,8 @@ export function registerEsqlFunction({ { name: 'execute_query', contexts: ['core'], - visibility: FunctionVisibility.User, - description: 'Execute an ES|QL query.', + visibility: FunctionVisibility.AssistantOnly, + description: 'Display the results of an ES|QL query.', parameters: { type: 'object', additionalProperties: false, @@ -95,13 +99,12 @@ export function registerEsqlFunction({ return { content: response }; } ); - registerFunction( { - name: 'esql', + name: 'query', contexts: ['core'], - description: `This function answers ES|QL related questions including query generation and syntax/command questions.`, - visibility: FunctionVisibility.System, + description: `This function generates, executes and/or visualizes a query based on the user's request.`, + visibility: FunctionVisibility.AssistantOnly, parameters: { type: 'object', additionalProperties: false, @@ -137,15 +140,42 @@ export function registerEsqlFunction({ Extract data? Request \`DISSECT\` AND \`GROK\`. Convert a column based on a set of conditionals? Request \`EVAL\` and \`CASE\`. - Examples for determining whether the user wants to execute a query: - - "Show me the avg of x" - - "Give me the results of y" - - "Display the sum of z" + For determining the intention of the user, the following options are available: + + ${VisualizeESQLUserIntention.generateQueryOnly}: the user only wants to generate the query, + but not run it. + + ${VisualizeESQLUserIntention.executeAndReturnResults}: the user wants to execute the query, + and have the assistant return/analyze/summarize the results. they don't need a + visualization. + + ${VisualizeESQLUserIntention.visualizeAuto}: The user wants to visualize the data from the + query, but wants us to pick the best visualization type, or their preferred + visualization is unclear. + + These intentions will display a specific visualization: + ${VisualizeESQLUserIntention.visualizeBar} + ${VisualizeESQLUserIntention.visualizeDonut} + ${VisualizeESQLUserIntention.visualizeHeatmap} + ${VisualizeESQLUserIntention.visualizeLine} + ${VisualizeESQLUserIntention.visualizeTagcloud} + ${VisualizeESQLUserIntention.visualizeTreemap} + ${VisualizeESQLUserIntention.visualizeWaffle} + ${VisualizeESQLUserIntention.visualizeXy} + + Some examples: + "Show me the avg of x" => ${VisualizeESQLUserIntention.executeAndReturnResults} + "Show me the results of y" => ${VisualizeESQLUserIntention.executeAndReturnResults} + "Display the sum of z" => ${VisualizeESQLUserIntention.executeAndReturnResults} - Examples for determining whether the user does not want to execute a query: - - "I want a query that ..." - - "... Just show me the query" - - "Create a query that ..."` + "I want a query that ..." => ${VisualizeESQLUserIntention.generateQueryOnly} + "... Just show me the query" => ${VisualizeESQLUserIntention.generateQueryOnly} + "Create a query that ..." => ${VisualizeESQLUserIntention.generateQueryOnly} + + "Show me the avg of x over time" => ${VisualizeESQLUserIntention.visualizeAuto} + "I want a bar chart of ... " => ${VisualizeESQLUserIntention.visualizeBar} + "I want to see a heat map of ..." => ${VisualizeESQLUserIntention.visualizeHeatmap} + ` ), signal, functions: [ @@ -172,13 +202,13 @@ export function registerEsqlFunction({ }, description: 'A list of functions.', }, - execute: { - type: 'boolean', - description: - 'Whether the user wants to execute a query (true) or just wants the query to be displayed (false)', + intention: { + type: 'string', + description: `What the user\'s intention is.`, + enum: VISUALIZE_ESQL_USER_INTENTIONS, }, }, - required: ['commands', 'functions', 'execute'], + required: ['commands', 'functions', 'intention'], }, }, ], @@ -191,13 +221,29 @@ export function registerEsqlFunction({ const args = JSON.parse(response.message.function_call.arguments) as { commands: string[]; functions: string[]; - execute: boolean; + intention: VisualizeESQLUserIntention; }; const keywords = args.commands.concat(args.functions).concat('SYNTAX').concat('OVERVIEW'); const messagesToInclude = mapValues(pick(esqlDocs, keywords), ({ data }) => data); + let userIntentionMessage: string; + + switch (args.intention) { + case VisualizeESQLUserIntention.executeAndReturnResults: + userIntentionMessage = `When you generate a query, it will automatically be executed and its results returned to you. The user does not need to do anything for this.`; + break; + + case VisualizeESQLUserIntention.generateQueryOnly: + userIntentionMessage = `Any generated query will not be executed automatically, the user needs to do this themselves.`; + break; + + default: + userIntentionMessage = `The generated query will automatically be visualized to the user, displayed below your message. The user does not need to do anything for this.`; + break; + } + const esqlResponse$: Observable = await client.chat( 'answer_esql_question', { @@ -210,6 +256,8 @@ export function registerEsqlFunction({ Prefer to use commands and functions for which you have requested documentation. + ${userIntentionMessage} + DO NOT UNDER ANY CIRCUMSTANCES use commands or functions that are not a capability of ES|QL as mentioned in the system message and documentation. @@ -266,11 +314,13 @@ export function registerEsqlFunction({ message: { ...msg.message, content: correctCommonEsqlMistakes(msg.message.content, resources.logger), - ...(esqlQuery && args.execute + ...(esqlQuery && + args.intention && + args.intention !== VisualizeESQLUserIntention.generateQueryOnly ? { function_call: { - name: 'execute_query', - arguments: JSON.stringify({ query: esqlQuery }), + name: 'visualize_query', + arguments: JSON.stringify({ query: esqlQuery, intention: args.intention }), trigger: MessageRole.Assistant as const, }, } diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/esql/system_message.txt b/x-pack/plugins/observability_ai_assistant/server/functions/query/system_message.txt similarity index 100% rename from x-pack/plugins/observability_ai_assistant/server/functions/esql/system_message.txt rename to x-pack/plugins/observability_ai_assistant/server/functions/query/system_message.txt diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/visualize_esql.ts b/x-pack/plugins/observability_ai_assistant/server/functions/visualize_esql.ts new file mode 100644 index 0000000000000..d4a1f867e3868 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/functions/visualize_esql.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 { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; +import type { ESQLSearchReponse } from '@kbn/es-types'; +import { visualizeESQLFunction } from '../../common/functions/visualize_esql'; +import type { FunctionRegistrationParameters } from '.'; + +export function registerVisualizeESQLFunction({ + client, + registerFunction, + resources, +}: FunctionRegistrationParameters) { + registerFunction( + visualizeESQLFunction, + async ({ arguments: { query }, connectorId, messages }, signal) => { + // With limit 0 I get only the columns, it is much more performant + const performantQuery = `${query} | limit 0`; + const coreContext = await resources.context.core; + + const response = (await ( + await coreContext + ).elasticsearch.client.asCurrentUser.transport.request({ + method: 'POST', + path: '_query', + body: { + query: performantQuery, + }, + })) as ESQLSearchReponse; + const columns = + response.columns?.map(({ name, type }) => ({ + id: name, + name, + meta: { type: esFieldTypeToKibanaFieldType(type) }, + })) ?? []; + return { content: columns }; + } + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts index e1d49696e5113..202df11f8faa4 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts @@ -15,7 +15,7 @@ import type { Message, } from '../../../common/types'; import { filterFunctionDefinitions } from '../../../common/utils/filter_function_definitions'; -import { FunctionHandler, FunctionHandlerRegistry } from '../types'; +import type { FunctionHandler, FunctionHandlerRegistry } from '../types'; export class FunctionArgsValidationError extends Error { constructor(public readonly errors: ErrorObject[]) { @@ -45,7 +45,10 @@ export class ChatFunctionClient { getFunctions({ contexts, filter, - }: { contexts?: string[]; filter?: string } = {}): FunctionHandler[] { + }: { + contexts?: string[]; + filter?: string; + } = {}): FunctionHandler[] { const allFunctions = Array.from(this.functionRegistry.values()); const functionsByName = keyBy(allFunctions, (definition) => definition.definition.name); 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 ad208927b7636..76749e75daed1 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 @@ -28,6 +28,7 @@ import { } from '../../../common/conversation_complete'; import { FunctionResponse, + FunctionVisibility, MessageRole, type CompatibleJSONSchema, type Conversation, @@ -205,6 +206,13 @@ export class ObservabilityAIAssistantClient { ? [] : functionClient .getFunctions() + .filter((fn) => { + const visibility = fn.definition.visibility ?? FunctionVisibility.All; + return ( + visibility === FunctionVisibility.All || + visibility === FunctionVisibility.AssistantOnly + ); + }) .map((fn) => pick(fn.definition, 'name', 'description', 'parameters')), } ) diff --git a/x-pack/plugins/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_ai_assistant/tsconfig.json index 183da3706ce54..f5a29c470fe7a 100644 --- a/x-pack/plugins/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_ai_assistant/tsconfig.json @@ -60,7 +60,13 @@ "@kbn/expect", "@kbn/apm-synthtrace-client", "@kbn/apm-synthtrace", - "@kbn/code-editor" + "@kbn/code-editor", + "@kbn/ui-actions-plugin", + "@kbn/expressions-plugin", + "@kbn/visualization-utils", + "@kbn/field-types", + "@kbn/es-types", + "@kbn/esql-utils" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 4c35a928b3f5d..a56068d941fe6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29214,17 +29214,12 @@ "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "Système", "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "Vous", "xpack.observabilityAiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", - "xpack.observabilityAiAssistant.confirmDeleteButtonText": "Supprimer la conversation", - "xpack.observabilityAiAssistant.confirmDeleteConversationContent": "Cette action ne peut pas être annulée.", - "xpack.observabilityAiAssistant.confirmDeleteConversationTitle": "Supprimer cette conversation ?", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "Connecteur :", "xpack.observabilityAiAssistant.connectorSelector.empty": "Aucun connecteur", "xpack.observabilityAiAssistant.connectorSelector.error": "Impossible de charger les connecteurs", - "xpack.observabilityAiAssistant.conversationDeepLinkLabel": "Conversation ouverte", "xpack.observabilityAiAssistant.conversationList.errorMessage": "Échec de chargement", "xpack.observabilityAiAssistant.conversationList.noConversations": "Aucune conversation", "xpack.observabilityAiAssistant.conversationList.title": "Précédemment", - "xpack.observabilityAiAssistant.conversationListDeepLinkLabel": "Accéder aux conversations", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "Conversations", "xpack.observabilityAiAssistant.conversationStartTitle": "a démarré une conversation", "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "Conversation introuvable", @@ -29234,7 +29229,6 @@ "xpack.observabilityAiAssistant.experimentalTitle": "Version d'évaluation technique", "xpack.observabilityAiAssistant.experimentalTooltip": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.", "xpack.observabilityAiAssistant.failedLoadingResponseText": "Échec de chargement de la réponse", - "xpack.observabilityAiAssistant.failedToDeleteConversation": "Impossible de supprimer la conversation", "xpack.observabilityAiAssistant.failedToGetStatus": "Échec de l'obtention du statut du modèle.", "xpack.observabilityAiAssistant.failedToLoadResponse": "Échec du chargement d'une réponse de l'assistant d'intelligence artificielle", "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "Échec de la configuration de la base de connaissances.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6acca4e21b517..49ad0731d747e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29215,17 +29215,12 @@ "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "システム", "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "あなた", "xpack.observabilityAiAssistant.checkingKbAvailability": "ナレッジベースの利用可能性を確認中", - "xpack.observabilityAiAssistant.confirmDeleteButtonText": "会話を削除", - "xpack.observabilityAiAssistant.confirmDeleteConversationContent": "この操作は元に戻すことができません。", - "xpack.observabilityAiAssistant.confirmDeleteConversationTitle": "この会話を削除しますか?", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "コネクター:", "xpack.observabilityAiAssistant.connectorSelector.empty": "コネクターなし", "xpack.observabilityAiAssistant.connectorSelector.error": "コネクターを読み込めませんでした", - "xpack.observabilityAiAssistant.conversationDeepLinkLabel": "会話を開く", "xpack.observabilityAiAssistant.conversationList.errorMessage": "の読み込みに失敗しました", "xpack.observabilityAiAssistant.conversationList.noConversations": "会話なし", "xpack.observabilityAiAssistant.conversationList.title": "以前", - "xpack.observabilityAiAssistant.conversationListDeepLinkLabel": "会話に移動", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "会話", "xpack.observabilityAiAssistant.conversationStartTitle": "会話を開始しました", "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "会話が見つかりません", @@ -29235,7 +29230,6 @@ "xpack.observabilityAiAssistant.experimentalTitle": "テクニカルプレビュー", "xpack.observabilityAiAssistant.experimentalTooltip": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。", "xpack.observabilityAiAssistant.failedLoadingResponseText": "応答の読み込みに失敗しました", - "xpack.observabilityAiAssistant.failedToDeleteConversation": "会話を削除できませんでした", "xpack.observabilityAiAssistant.failedToGetStatus": "モデルステータスを取得できませんでした。", "xpack.observabilityAiAssistant.failedToLoadResponse": "AIアシスタントからの応答を読み込めませんでした", "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "ナレッジベースをセットアップできませんでした。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 620655bf4f6ba..2aa3a1f8dff01 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29199,17 +29199,12 @@ "xpack.observabilityAiAssistant.chatTimeline.messages.system.label": "系统", "xpack.observabilityAiAssistant.chatTimeline.messages.user.label": "您", "xpack.observabilityAiAssistant.checkingKbAvailability": "正在检查知识库的可用性", - "xpack.observabilityAiAssistant.confirmDeleteButtonText": "删除对话", - "xpack.observabilityAiAssistant.confirmDeleteConversationContent": "此操作无法撤消。", - "xpack.observabilityAiAssistant.confirmDeleteConversationTitle": "删除此对话?", "xpack.observabilityAiAssistant.connectorSelector.connectorSelectLabel": "连接器:", "xpack.observabilityAiAssistant.connectorSelector.empty": "无连接器", "xpack.observabilityAiAssistant.connectorSelector.error": "无法加载连接器", - "xpack.observabilityAiAssistant.conversationDeepLinkLabel": "开放式对话", "xpack.observabilityAiAssistant.conversationList.errorMessage": "无法加载", "xpack.observabilityAiAssistant.conversationList.noConversations": "无对话", "xpack.observabilityAiAssistant.conversationList.title": "以前", - "xpack.observabilityAiAssistant.conversationListDeepLinkLabel": "前往对话", "xpack.observabilityAiAssistant.conversationsDeepLinkTitle": "对话", "xpack.observabilityAiAssistant.conversationStartTitle": "已开始对话", "xpack.observabilityAiAssistant.couldNotFindConversationTitle": "未找到对话", @@ -29219,7 +29214,6 @@ "xpack.observabilityAiAssistant.experimentalTitle": "技术预览", "xpack.observabilityAiAssistant.experimentalTooltip": "此功能处于技术预览状态,在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。", "xpack.observabilityAiAssistant.failedLoadingResponseText": "无法加载响应", - "xpack.observabilityAiAssistant.failedToDeleteConversation": "无法删除对话", "xpack.observabilityAiAssistant.failedToGetStatus": "无法获取模型状态。", "xpack.observabilityAiAssistant.failedToLoadResponse": "无法加载来自 AI 助手的响应", "xpack.observabilityAiAssistant.failedToSetupKnowledgeBase": "无法设置知识库。",