From f644ada48bbf0caf607a99de7842668f1e76a058 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:23:53 +0800 Subject: [PATCH 01/10] Add feature flag to control query assistant like features (#219) (#220) (#225) (cherry picked from commit 3ab558e4bee55581b1aab674cb439b056881b3eb) (cherry picked from commit 7b2374f6962ce14d1d424052e8d1bcef1d267fb0) Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- common/types/config.ts | 3 +++ public/plugin.tsx | 1 + public/types.ts | 4 ++++ server/index.ts | 1 + 4 files changed, 9 insertions(+) diff --git a/common/types/config.ts b/common/types/config.ts index 78f31871..bf84a8da 100644 --- a/common/types/config.ts +++ b/common/types/config.ts @@ -14,6 +14,9 @@ export const configSchema = schema.object({ incontextInsight: schema.object({ enabled: schema.boolean({ defaultValue: true }), }), + next: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), }); export type ConfigSchema = TypeOf; diff --git a/public/plugin.tsx b/public/plugin.tsx index 89c4f545..7585e0e5 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -147,6 +147,7 @@ export class AssistantPlugin actionExecutors[actionType] = execute; }, chatEnabled: () => this.config.chat.enabled, + nextEnabled: () => this.config.next.enabled, assistantActions, registerIncontextInsight: this.incontextInsightRegistry.register.bind( this.incontextInsightRegistry diff --git a/public/types.ts b/public/types.ts index 4b05b4ad..c9417680 100644 --- a/public/types.ts +++ b/public/types.ts @@ -46,6 +46,10 @@ export interface AssistantSetup { * Returns true if chat UI is enabled. */ chatEnabled: () => boolean; + /** + * Returns true if contextual assistant is enabled. + */ + nextEnabled: () => boolean; assistantActions: Omit; registerIncontextInsight: IncontextInsightRegistry['register']; renderIncontextInsight: (component: React.ReactNode) => React.ReactNode; diff --git a/server/index.ts b/server/index.ts index 74696c1a..4a0d1bab 100644 --- a/server/index.ts +++ b/server/index.ts @@ -11,6 +11,7 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { chat: true, incontextInsight: true, + next: true, }, schema: configSchema, }; From d7b680e4f7eef323ead2cd51fc944231ae04e5ef Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 18:21:49 +0800 Subject: [PATCH 02/10] feat: text to visualization (#218) (#223) * feat: add text to visualization Signed-off-by: Yulong Ruan --------- Signed-off-by: Yulong Ruan (cherry picked from commit 63d9d800c7cbc81376a8fbc8bc805644378791af) Signed-off-by: github-actions[bot] # Conflicts: # CHANGELOG.md Co-authored-by: github-actions[bot] Co-authored-by: Yulong Ruan --- common/constants/llm.ts | 5 + opensearch_dashboards.json | 7 +- .../visualization/source_selector.tsx | 102 +++++++ public/components/visualization/text2vega.ts | 163 ++++++++++ public/components/visualization/text2viz.scss | 10 + public/components/visualization/text2viz.tsx | 282 ++++++++++++++++++ .../visualization/text2viz_empty.tsx | 33 ++ .../visualization/text2viz_loading.tsx | 23 ++ public/plugin.tsx | 52 +++- public/text2viz.tsx | 24 ++ public/types.ts | 15 + server/plugin.ts | 6 + server/routes/get_agent.ts | 25 ++ server/routes/text2viz_routes.ts | 106 +++++++ .../services/chat/olly_chat_service.test.ts | 2 +- server/services/chat/olly_chat_service.ts | 17 +- 16 files changed, 853 insertions(+), 19 deletions(-) create mode 100644 public/components/visualization/source_selector.tsx create mode 100644 public/components/visualization/text2vega.ts create mode 100644 public/components/visualization/text2viz.scss create mode 100644 public/components/visualization/text2viz.tsx create mode 100644 public/components/visualization/text2viz_empty.tsx create mode 100644 public/components/visualization/text2viz_loading.tsx create mode 100644 public/text2viz.tsx create mode 100644 server/routes/get_agent.ts create mode 100644 server/routes/text2viz_routes.ts diff --git a/common/constants/llm.ts b/common/constants/llm.ts index 6e21fd94..63ac5a59 100644 --- a/common/constants/llm.ts +++ b/common/constants/llm.ts @@ -19,6 +19,11 @@ export const ASSISTANT_API = { ACCOUNT: `${API_BASE}/account`, } as const; +export const TEXT2VIZ_API = { + TEXT2PPL: `${API_BASE}/text2ppl`, + TEXT2VEGA: `${API_BASE}/text2vega`, +}; + export const NOTEBOOK_API = { CREATE_NOTEBOOK: `${NOTEBOOK_PREFIX}/note`, SET_PARAGRAPH: `${NOTEBOOK_PREFIX}/set_paragraphs/`, diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 84e7bffb..17370acd 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -5,16 +5,19 @@ "server": true, "ui": true, "requiredPlugins": [ + "data", "dashboard", "embeddable", "opensearchDashboardsReact", - "opensearchDashboardsUtils" + "opensearchDashboardsUtils", + "visualizations" ], "optionalPlugins": [ "dataSource", "dataSourceManagement" ], + "requiredBundles": [], "configPath": [ "assistant" ] -} \ No newline at end of file +} diff --git a/public/components/visualization/source_selector.tsx b/public/components/visualization/source_selector.tsx new file mode 100644 index 00000000..09dc5aca --- /dev/null +++ b/public/components/visualization/source_selector.tsx @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback, useMemo, useState, useEffect } from 'react'; +import { i18n } from '@osd/i18n'; + +import { useOpenSearchDashboards } from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { + DataSource, + DataSourceGroup, + DataSourceSelectable, + DataSourceOption, +} from '../../../../../src/plugins/data/public'; +import { StartServices } from '../../types'; + +export const SourceSelector = ({ + selectedSourceId, + onChange, +}: { + selectedSourceId: string; + onChange: (ds: DataSourceOption) => void; +}) => { + const { + services: { + data: { dataSources }, + notifications: { toasts }, + }, + } = useOpenSearchDashboards(); + const [currentDataSources, setCurrentDataSources] = useState([]); + const [dataSourceOptions, setDataSourceOptions] = useState([]); + + const selectedSources = useMemo(() => { + if (selectedSourceId) { + for (const group of dataSourceOptions) { + for (const item of group.options) { + if (item.value === selectedSourceId) { + return [item]; + } + } + } + } + return []; + }, [selectedSourceId, dataSourceOptions]); + + useEffect(() => { + if ( + !selectedSourceId && + dataSourceOptions.length > 0 && + dataSourceOptions[0].options.length > 0 + ) { + onChange(dataSourceOptions[0].options[0]); + } + }, [selectedSourceId, dataSourceOptions]); + + useEffect(() => { + const subscription = dataSources.dataSourceService.getDataSources$().subscribe((ds) => { + setCurrentDataSources(Object.values(ds)); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [dataSources]); + + const onDataSourceSelect = useCallback( + (selectedDataSources: DataSourceOption[]) => { + onChange(selectedDataSources[0]); + }, + [onChange] + ); + + const handleGetDataSetError = useCallback( + () => (error: Error) => { + toasts.addError(error, { + title: + i18n.translate('visualize.vega.failedToGetDataSetErrorDescription', { + defaultMessage: 'Failed to get data set: ', + }) + (error.message || error.name), + }); + }, + [toasts] + ); + + const memorizedReload = useCallback(() => { + dataSources.dataSourceService.reload(); + }, [dataSources.dataSourceService]); + + return ( + + ); +}; diff --git a/public/components/visualization/text2vega.ts b/public/components/visualization/text2vega.ts new file mode 100644 index 00000000..9621bd2b --- /dev/null +++ b/public/components/visualization/text2vega.ts @@ -0,0 +1,163 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { debounceTime, switchMap, tap, filter, catchError } from 'rxjs/operators'; +import { TEXT2VIZ_API } from '.../../../common/constants/llm'; +import { HttpSetup } from '../../../../../src/core/public'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; + +const DATA_SOURCE_DELIMITER = '::'; + +const topN = (ppl: string, n: number) => `${ppl} | head ${n}`; + +const getDataSourceAndIndexFromLabel = (label: string) => { + if (label.includes(DATA_SOURCE_DELIMITER)) { + return [ + label.slice(0, label.indexOf(DATA_SOURCE_DELIMITER)), + label.slice(label.indexOf(DATA_SOURCE_DELIMITER) + DATA_SOURCE_DELIMITER.length), + ] as const; + } + return [, label] as const; +}; + +interface Input { + prompt: string; + index: string; + dataSourceId?: string; +} + +export class Text2Vega { + input$ = new BehaviorSubject({ prompt: '', index: '' }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result$: Observable | { error: any }>; + status$ = new BehaviorSubject<'RUNNING' | 'STOPPED'>('STOPPED'); + http: HttpSetup; + searchClient: DataPublicPluginStart['search']; + + constructor(http: HttpSetup, searchClient: DataPublicPluginStart['search']) { + this.http = http; + this.searchClient = searchClient; + this.result$ = this.input$ + .pipe( + filter((v) => v.prompt.length > 0), + debounceTime(200), + tap(() => this.status$.next('RUNNING')) + ) + .pipe( + switchMap((v) => + of(v).pipe( + // text to ppl + switchMap(async (value) => { + const [, indexName] = getDataSourceAndIndexFromLabel(value.index); + const pplQuestion = value.prompt.split('//')[0]; + const ppl = await this.text2ppl(pplQuestion, indexName, value.dataSourceId); + return { + ...value, + ppl, + }; + }), + // query sample data with ppl + switchMap(async (value) => { + const ppl = topN(value.ppl, 2); + const res = await this.searchClient + .search( + { params: { body: { query: ppl } }, dataSourceId: value.dataSourceId }, + { strategy: 'pplraw' } + ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .toPromise(); + return { ...value, sample: res.rawResponse }; + }), + // call llm to generate vega + switchMap(async (value) => { + const result = await this.text2vega({ + input: value.prompt, + ppl: value.ppl, + sampleData: JSON.stringify(value.sample.jsonData), + dataSchema: JSON.stringify(value.sample.schema), + dataSourceId: value.dataSourceId, + }); + const [dataSourceName] = getDataSourceAndIndexFromLabel(value.index); + result.data = { + url: { + '%type%': 'ppl', + body: { query: value.ppl }, + data_source_name: dataSourceName, + }, + }; + return result; + }), + catchError((e) => of({ error: e })) + ) + ) + ) + .pipe(tap(() => this.status$.next('STOPPED'))); + } + + async text2vega({ + input, + ppl, + sampleData, + dataSchema, + dataSourceId, + }: { + input: string; + ppl: string; + sampleData: string; + dataSchema: string; + dataSourceId?: string; + }) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const escapeField = (json: any, field: string) => { + if (json[field]) { + if (typeof json[field] === 'string') { + json[field] = json[field].replace(/\./g, '\\.'); + } + if (typeof json[field] === 'object') { + Object.keys(json[field]).forEach((p) => { + escapeField(json[field], p); + }); + } + } + }; + const res = await this.http.post(TEXT2VIZ_API.TEXT2VEGA, { + body: JSON.stringify({ + input, + ppl, + sampleData: JSON.stringify(sampleData), + dataSchema: JSON.stringify(dataSchema), + }), + query: { dataSourceId }, + }); + + // need to escape field: geo.city -> field: geo\\.city + escapeField(res, 'encoding'); + return res; + } + + async text2ppl(query: string, index: string, dataSourceId?: string) { + const pplResponse = await this.http.post(TEXT2VIZ_API.TEXT2PPL, { + body: JSON.stringify({ + question: query, + index, + }), + query: { dataSourceId }, + }); + return pplResponse.ppl; + } + + invoke(value: Input) { + this.input$.next(value); + } + + getStatus$() { + return this.status$; + } + + getResult$() { + return this.result$; + } +} diff --git a/public/components/visualization/text2viz.scss b/public/components/visualization/text2viz.scss new file mode 100644 index 00000000..ead70f30 --- /dev/null +++ b/public/components/visualization/text2viz.scss @@ -0,0 +1,10 @@ +.text2viz__page { + .visualize { + height: 400px; + } + + .text2viz__right { + padding-top: 15px; + padding-left: 30px; + } +} diff --git a/public/components/visualization/text2viz.tsx b/public/components/visualization/text2viz.tsx new file mode 100644 index 00000000..8a4ead6c --- /dev/null +++ b/public/components/visualization/text2viz.tsx @@ -0,0 +1,282 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPageBody, + EuiPage, + EuiPageContent, + EuiPageContentBody, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiIcon, + EuiButtonIcon, + EuiButton, + EuiBreadcrumb, + EuiHeaderLinks, +} from '@elastic/eui'; +import React, { useEffect, useRef, useState } from 'react'; +import { i18n } from '@osd/i18n'; + +import { useCallback } from 'react'; +import { useObservable } from 'react-use'; +import { useMemo } from 'react'; +import { SourceSelector } from './source_selector'; +import type { DataSourceOption } from '../../../../../src/plugins/data/public'; +import chatIcon from '../../assets/chat.svg'; +import { EmbeddableRenderer } from '../../../../../src/plugins/embeddable/public'; +import { + useOpenSearchDashboards, + MountPointPortal, + toMountPoint, +} from '../../../../../src/plugins/opensearch_dashboards_react/public'; +import { StartServices } from '../../types'; +import { + VISUALIZE_EMBEDDABLE_TYPE, + VisSavedObject, + VisualizeInput, +} from '../../../../../src/plugins/visualizations/public'; +import './text2viz.scss'; +import { Text2VizEmpty } from './text2viz_empty'; +import { Text2VizLoading } from './text2viz_loading'; +import { Text2Vega } from './text2vega'; +import { + OnSaveProps, + SavedObjectSaveModalOrigin, +} from '../../../../../src/plugins/saved_objects/public'; + +export const Text2Viz = () => { + const [selectedSource, setSelectedSource] = useState(); + const { + services: { + application, + chrome, + embeddable, + visualizations, + http, + notifications, + setHeaderActionMenu, + overlays, + data, + }, + } = useOpenSearchDashboards(); + const [input, setInput] = useState(''); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [vegaSpec, setVegaSpec] = useState>(); + const text2vegaRef = useRef(new Text2Vega(http, data.search)); + const status = useObservable(text2vegaRef.current.status$); + + useEffect(() => { + const text2vega = text2vegaRef.current; + const subscription = text2vega.getResult$().subscribe((result) => { + if (result) { + if (result.error) { + notifications.toasts.addError(result.error, { + title: i18n.translate('dashboardAssistant.feature.text2viz.error', { + defaultMessage: 'Error while executing text to vega', + }), + }); + } else { + setVegaSpec(result); + } + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, [http, notifications]); + + const onInputChange = useCallback((e: React.ChangeEvent) => { + setInput(e.target.value); + }, []); + + const onSubmit = useCallback(async () => { + setVegaSpec(undefined); + const text2vega = text2vegaRef.current; + if (selectedSource?.label) { + const dataSource = (await selectedSource.ds.getDataSet()).dataSets.find( + (ds) => ds.title === selectedSource.label + ); + text2vega.invoke({ + index: selectedSource.label, + prompt: input, + dataSourceId: dataSource?.dataSourceId, + }); + } + }, [selectedSource, input]); + + const factory = embeddable.getEmbeddableFactory(VISUALIZE_EMBEDDABLE_TYPE); + const vis = useMemo(() => { + return vegaSpec + ? visualizations.convertToSerializedVis({ + title: vegaSpec?.title ?? 'vega', + description: vegaSpec?.description ?? '', + visState: { + title: vegaSpec?.title ?? 'vega', + type: 'vega', + aggs: [], + params: { + spec: JSON.stringify(vegaSpec, null, 4), + }, + }, + }) + : null; + }, [vegaSpec]); + + const onSaveClick = useCallback(async () => { + if (!vis) return; + + const doSave = async (onSaveProps: OnSaveProps) => { + const savedVis: VisSavedObject = await visualizations.savedVisualizationsLoader.get(); // .createVis('vega', vis) + savedVis.visState = { + title: onSaveProps.newTitle, + type: vis.type, + params: vis.params, + aggs: [], + }; + savedVis.title = onSaveProps.newTitle; + savedVis.description = onSaveProps.newDescription; + savedVis.copyOnSave = onSaveProps.newCopyOnSave; + try { + const id = await savedVis.save({ + isTitleDuplicateConfirmed: onSaveProps.isTitleDuplicateConfirmed, + onTitleDuplicate: onSaveProps.onTitleDuplicate, + }); + if (id) { + notifications.toasts.addSuccess({ + title: i18n.translate('dashboardAssistant.feature.text2viz.saveSuccess', { + defaultMessage: `Saved '{title}'`, + values: { + title: savedVis.title, + }, + }), + }); + dialog.close(); + } + } catch (e) { + notifications.toasts.addDanger({ + title: i18n.translate('dashboardAssistant.feature.text2viz.saveFail', { + defaultMessage: `Error on saving '{title}'`, + values: { + title: savedVis.title, + }, + }), + }); + } + }; + + const dialog = overlays.openModal( + toMountPoint( + dialog.close()} + onSave={doSave} + /> + ) + ); + }, [vis, visualizations, notifications]); + + useEffect(() => { + const breadcrumbs: EuiBreadcrumb[] = [ + { + text: 'Visualize', + onClick: () => { + application.navigateToApp('visualize'); + }, + }, + { + text: 'Create', + }, + ]; + chrome.setBreadcrumbs(breadcrumbs); + }, [chrome, application]); + + return ( + + + + + {i18n.translate('dashboardAssistant.feature.text2viz.save', { + defaultMessage: 'Save', + })} + + + + + + + + + setSelectedSource(ds)} + /> + + + } + placeholder="Generate visualization with a natural language question." + /> + + + + + + {status === 'STOPPED' && !vegaSpec && ( + + + + + + )} + {status === 'RUNNING' && ( + + + + + + )} + {status === 'STOPPED' && vis && ( + + + {factory && ( + + )} + + + )} + + + + + ); +}; diff --git a/public/components/visualization/text2viz_empty.tsx b/public/components/visualization/text2viz_empty.tsx new file mode 100644 index 00000000..f158f6a1 --- /dev/null +++ b/public/components/visualization/text2viz_empty.tsx @@ -0,0 +1,33 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export const Text2VizEmpty = () => { + return ( + + {i18n.translate('dashboardAssistant.feature.text2viz.getStarted', { + defaultMessage: 'Get started', + })} + + } + body={ + <> +

+ {i18n.translate('dashboardAssistant.feature.text2viz.body', { + defaultMessage: + 'Use the Natural Language Query form field to automatically generate visualizations using simple conversational prompts.', + })} +

+ + } + /> + ); +}; diff --git a/public/components/visualization/text2viz_loading.tsx b/public/components/visualization/text2viz_loading.tsx new file mode 100644 index 00000000..8d21fd80 --- /dev/null +++ b/public/components/visualization/text2viz_loading.tsx @@ -0,0 +1,23 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export const Text2VizLoading = () => { + return ( + } + title={ +

+ {i18n.translate('dashboardAssistant.feature.text2viz.loading', { + defaultMessage: 'Generating Visualization', + })} +

+ } + /> + ); +}; diff --git a/public/plugin.tsx b/public/plugin.tsx index 7585e0e5..e1309fe0 100644 --- a/public/plugin.tsx +++ b/public/plugin.tsx @@ -3,10 +3,18 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { i18n } from '@osd/i18n'; import { EuiLoadingSpinner } from '@elastic/eui'; import React, { lazy, Suspense } from 'react'; import { Subscription } from 'rxjs'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '../../../src/core/public'; +import { + AppMountParameters, + AppNavLinkStatus, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '../../../src/core/public'; import { createOpenSearchDashboardsReactContext, toMountPoint, @@ -94,6 +102,48 @@ export class AssistantPlugin dataSourceManagement: setupDeps.dataSourceManagement, }); + if (this.config.next.enabled) { + setupDeps.visualizations.registerAlias({ + name: 'text2viz', + aliasPath: '#/', + aliasApp: 'text2viz', + title: i18n.translate('dashboardAssistant.feature.text2viz.title', { + defaultMessage: 'Natural language', + }), + description: i18n.translate('dashboardAssistant.feature.text2viz.description', { + defaultMessage: 'Generate visualization with a natural language question.', + }), + icon: 'chatRight', + stage: 'experimental', + promotion: { + buttonText: i18n.translate('dashboardAssistant.feature.text2viz.promotion.buttonText', { + defaultMessage: 'Natural language previewer', + }), + description: i18n.translate('dashboardAssistant.feature.text2viz.promotion.description', { + defaultMessage: + 'Not sure which visualization to choose? Generate visualization previews with a natural language question.', + }), + }, + }); + + core.application.register({ + id: 'text2viz', + title: i18n.translate('dashboardAssistant.feature.text2viz', { + defaultMessage: 'Natural language previewer', + }), + navLinkStatus: AppNavLinkStatus.hidden, + mount: async (params: AppMountParameters) => { + const [coreStart, pluginsStart] = await core.getStartServices(); + const { renderText2VizApp } = await import('./text2viz'); + return renderText2VizApp(params, { + ...coreStart, + ...pluginsStart, + setHeaderActionMenu: params.setHeaderActionMenu, + }); + }, + }); + } + if (this.config.chat.enabled) { const setupChat = async () => { const [coreStart, startDeps] = await core.getStartServices(); diff --git a/public/text2viz.tsx b/public/text2viz.tsx new file mode 100644 index 00000000..634cc191 --- /dev/null +++ b/public/text2viz.tsx @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters } from '../../../src/core/public'; +import { Text2Viz } from './components/visualization/text2viz'; +import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public'; +import { StartServices } from './types'; + +export const renderText2VizApp = (params: AppMountParameters, services: StartServices) => { + ReactDOM.render( + + + , + + params.element + ); + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; +}; diff --git a/public/types.ts b/public/types.ts index c9417680..aa95eef6 100644 --- a/public/types.ts +++ b/public/types.ts @@ -10,6 +10,12 @@ import { IChatContext } from './contexts/chat_context'; import { MessageContentProps } from './tabs/chat/messages/message_content'; import { DataSourceServiceContract, IncontextInsightRegistry } from './services'; import { DataSourceManagementPluginSetup } from '../../../src/plugins/data_source_management/public'; +import { + VisualizationsSetup, + VisualizationsStart, +} from '../../../src/plugins/visualizations/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../src/plugins/data/public'; +import { AppMountParameters, CoreStart } from '../../../src/core/public'; export interface RenderProps { props: MessageContentProps; @@ -29,11 +35,15 @@ export interface AssistantActions { } export interface AssistantPluginStartDependencies { + data: DataPublicPluginStart; + visualizations: VisualizationsStart; embeddable: EmbeddableStart; dashboard: DashboardStart; } export interface AssistantPluginSetupDependencies { + data: DataPublicPluginSetup; + visualizations: VisualizationsSetup; embeddable: EmbeddableSetup; dataSourceManagement?: DataSourceManagementPluginSetup; } @@ -59,6 +69,11 @@ export interface AssistantStart { dataSource: DataSourceServiceContract; } +export type StartServices = CoreStart & + AssistantPluginStartDependencies & { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + }; + export interface UserAccount { username: string; } diff --git a/server/plugin.ts b/server/plugin.ts index 5918864a..48777819 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -16,6 +16,7 @@ import { AssistantPluginSetup, AssistantPluginStart, MessageParser } from './typ import { BasicInputOutputParser } from './parsers/basic_input_output_parser'; import { VisualizationCardParser } from './parsers/visualization_card_parser'; import { registerChatRoutes } from './routes/chat_routes'; +import { registerText2VizRoutes } from './routes/text2viz_routes'; export class AssistantPlugin implements Plugin { private readonly logger: Logger; @@ -47,6 +48,11 @@ export class AssistantPlugin implements Plugin ({ observability: { show: true, diff --git a/server/routes/get_agent.ts b/server/routes/get_agent.ts new file mode 100644 index 00000000..a8d285a6 --- /dev/null +++ b/server/routes/get_agent.ts @@ -0,0 +1,25 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OpenSearchClient } from '../../../../src/core/server'; +import { ML_COMMONS_BASE_API } from '../utils/constants'; + +export const getAgent = async (id: string, client: OpenSearchClient['transport']) => { + try { + const path = `${ML_COMMONS_BASE_API}/config/${id}`; + const response = await client.request({ + method: 'GET', + path, + }); + + if (!response || !response.body.configuration?.agent_id) { + throw new Error(`cannot get agent ${id} by calling the api: ${path}`); + } + return response.body.configuration.agent_id; + } catch (error) { + const errorMessage = JSON.stringify(error.meta?.body) || error; + throw new Error(`get agent ${id} failed, reason: ${errorMessage}`); + } +}; diff --git a/server/routes/text2viz_routes.ts b/server/routes/text2viz_routes.ts new file mode 100644 index 00000000..bf7d58ef --- /dev/null +++ b/server/routes/text2viz_routes.ts @@ -0,0 +1,106 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter } from '../../../../src/core/server'; +import { TEXT2VIZ_API } from '../../common/constants/llm'; +import { getOpenSearchClientTransport } from '../utils/get_opensearch_client_transport'; +import { ML_COMMONS_BASE_API } from '../utils/constants'; +import { getAgent } from './get_agent'; + +const TEXT2VEGA_AGENT_CONFIG_ID = 'text2vega'; +const TEXT2PPL_AGENT_CONFIG_ID = 'text2ppl'; + +export function registerText2VizRoutes(router: IRouter) { + router.post( + { + path: TEXT2VIZ_API.TEXT2VEGA, + validate: { + body: schema.object({ + input: schema.string(), + ppl: schema.string(), + dataSchema: schema.string(), + sampleData: schema.string(), + }), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const client = await getOpenSearchClientTransport({ + context, + dataSourceId: req.query.dataSourceId, + }); + const agentId = await getAgent(TEXT2VEGA_AGENT_CONFIG_ID, client); + const response = await client.request({ + method: 'POST', + path: `${ML_COMMONS_BASE_API}/agents/${agentId}/_execute`, + body: { + parameters: { + input: req.body.input, + ppl: req.body.ppl, + dataSchema: req.body.dataSchema, + sampleData: req.body.sampleData, + }, + }, + }); + + try { + // let result = response.body.inference_results[0].output[0].dataAsMap; + let result = JSON.parse(response.body.inference_results[0].output[0].result); + // sometimes llm returns {response: } instead of + if (result.response) { + result = JSON.parse(result.response); + } + // Sometimes the response contains width and height which is not needed, here delete the these fields + delete result.width; + delete result.height; + + return res.ok({ body: result }); + } catch (e) { + return res.internalError(); + } + }) + ); + + router.post( + { + path: TEXT2VIZ_API.TEXT2PPL, + validate: { + body: schema.object({ + index: schema.string(), + question: schema.string(), + }), + query: schema.object({ + dataSourceId: schema.maybe(schema.string()), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const client = await getOpenSearchClientTransport({ + context, + dataSourceId: req.query.dataSourceId, + }); + const agentId = await getAgent(TEXT2PPL_AGENT_CONFIG_ID, client); + const response = await client.request({ + method: 'POST', + path: `${ML_COMMONS_BASE_API}/agents/${agentId}/_execute`, + body: { + parameters: { + question: req.body.question, + index: req.body.index, + }, + }, + }); + try { + const result = JSON.parse(response.body.inference_results[0].output[0].result); + return res.ok({ body: result }); + } catch (e) { + return res.internalError(); + } + }) + ); +} diff --git a/server/services/chat/olly_chat_service.test.ts b/server/services/chat/olly_chat_service.test.ts index 49362dcb..c42fca7f 100644 --- a/server/services/chat/olly_chat_service.test.ts +++ b/server/services/chat/olly_chat_service.test.ts @@ -233,7 +233,7 @@ describe('OllyChatService', () => { interactionId: 'interactionId', }) ).rejects.toMatchInlineSnapshot( - `[Error: get root agent failed, reason: Error: cannot get root agent by calling the api: /_plugins/_ml/config/os_chat]` + `[Error: get agent os_chat failed, reason: Error: cannot get agent os_chat by calling the api: /_plugins/_ml/config/os_chat]` ); }); }); diff --git a/server/services/chat/olly_chat_service.ts b/server/services/chat/olly_chat_service.ts index bdebec5a..830f6c4f 100644 --- a/server/services/chat/olly_chat_service.ts +++ b/server/services/chat/olly_chat_service.ts @@ -8,6 +8,7 @@ import { OpenSearchClient } from '../../../../../src/core/server'; import { IMessage, IInput } from '../../../common/types/chat_saved_object_attributes'; import { ChatService } from './chat_service'; import { ML_COMMONS_BASE_API, ROOT_AGENT_CONFIG_ID } from '../../utils/constants'; +import { getAgent } from '../../routes/get_agent'; interface AgentRunPayload { question?: string; @@ -25,21 +26,7 @@ export class OllyChatService implements ChatService { constructor(private readonly opensearchClientTransport: OpenSearchClient['transport']) {} private async getRootAgent(): Promise { - try { - const path = `${ML_COMMONS_BASE_API}/config/${ROOT_AGENT_CONFIG_ID}`; - const response = await this.opensearchClientTransport.request({ - method: 'GET', - path, - }); - - if (!response || !response.body.configuration?.agent_id) { - throw new Error(`cannot get root agent by calling the api: ${path}`); - } - return response.body.configuration.agent_id; - } catch (error) { - const errorMessage = JSON.stringify(error.meta?.body) || error; - throw new Error('get root agent failed, reason: ' + errorMessage); - } + return await getAgent(ROOT_AGENT_CONFIG_ID, this.opensearchClientTransport); } private async requestAgentRun(payload: AgentRunPayload) { From 444e985787cd8a5d2167bdd365b7e0819a81b2c4 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:15:54 +0800 Subject: [PATCH 03/10] added savedObjects to requiredPlugins (#231) (#233) (cherry picked from commit b7af70cf8597c027f0563002cec25c7b4199a3fa) Signed-off-by: Yulong Ruan Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- opensearch_dashboards.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 17370acd..67323557 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -10,7 +10,8 @@ "embeddable", "opensearchDashboardsReact", "opensearchDashboardsUtils", - "visualizations" + "visualizations", + "savedObjects" ], "optionalPlugins": [ "dataSource", From f78e36cee409e231aa04c9c58c2aa79a219f3d99 Mon Sep 17 00:00:00 2001 From: Xuesong Luo Date: Wed, 24 Jul 2024 11:10:54 +0800 Subject: [PATCH 04/10] Merge pull request #213 from opensearch-project/dependabot/npm_and_yarn/braces-3.0.3 build(deps): bump braces from 3.0.2 to 3.0.3 (cherry picked from commit 6c4f5e2c9524484475b7cdb79419d8f9aac68349) Signed-off-by: Cui Hailong --- yarn.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 86b22753..187f379f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -70,10 +70,10 @@ dependencies: "@types/node" "*" -"@types/dompurify@^2.3.3": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9" - integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg== +"@types/dompurify@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7" + integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg== dependencies: "@types/trusted-types" "*" @@ -309,11 +309,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" bs-logger@0.x: version "0.2.6" @@ -516,10 +516,10 @@ domexception@^4.0.0: dependencies: webidl-conversions "^7.0.0" -dompurify@^2.4.1: - version "2.4.7" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" - integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== +dompurify@^3.0.11: + version "3.1.5" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.5.tgz#2c6a113fc728682a0f55684b1388c58ddb79dc38" + integrity sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA== eastasianwidth@^0.2.0: version "0.2.0" @@ -726,10 +726,10 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" From 8bce8a6abd98ab87c9c9c3a9ce70cbc250016a4d Mon Sep 17 00:00:00 2001 From: Cui Hailong Date: Wed, 24 Jul 2024 06:51:42 +0000 Subject: [PATCH 05/10] add change log Signed-off-by: Cui Hailong --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f57ca14..91f09f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,3 +8,4 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Change implementation of basic_input_output to built-in parser ([#10](https://github.com/opensearch-project/dashboards-assistant/pull/10)) - Add interactions into ChatState and pass specific interaction into message_bubble ([#12](https://github.com/opensearch-project/dashboards-assistant/pull/12)) - Refactor the code to get root agent id by calling the API in ml-commons plugin ([#128](https://github.com/opensearch-project/dashboards-assistant/pull/128)) +- build(deps): bump braces from 3.0.2 to 3.0.3 ([#213](https://github.com/opensearch-project/dashboards-assistant/pull/213)) \ No newline at end of file From 18a197e15cbc931ad3b20ff9630a6ee28f69c649 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:46:41 +0800 Subject: [PATCH 06/10] add 2.16 release note (#227) (#235) * add release note * remove experimental in release note --------- (cherry picked from commit 4f33fa090e17a1ae52edb30110a15b224ab8b638) Signed-off-by: Yulong Ruan Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- .../dashboards-assistant.release-notes-2.16.0.0.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 release-notes/dashboards-assistant.release-notes-2.16.0.0.md diff --git a/release-notes/dashboards-assistant.release-notes-2.16.0.0.md b/release-notes/dashboards-assistant.release-notes-2.16.0.0.md new file mode 100644 index 00000000..c9804b9f --- /dev/null +++ b/release-notes/dashboards-assistant.release-notes-2.16.0.0.md @@ -0,0 +1,7 @@ +## Version 2.16.0.0 Release Notes + +Compatible with OpenSearch and OpenSearch Dashboards Version 2.16.0 + +### Features + +- Add feature to support text to visualization. ([#218](https://github.com/opensearch-project/dashboards-assistant/pull/218)) \ No newline at end of file From 8a325087fa25ec255cd8e40ffb292480f6b1da10 Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:58:57 +0800 Subject: [PATCH 07/10] switch to ml_configuration in agent response as backend changed (#239) (#243) * switch to ml_configuration in agent response as backend changed Signed-off-by: Heng Qian * Address comments and add changelog Signed-off-by: Heng Qian * Address comments Signed-off-by: Heng Qian * Add change in release notes Signed-off-by: Heng Qian --------- Signed-off-by: Heng Qian (cherry picked from commit 913c47b46b321410bd9baa972f225d8c197d92d8) Signed-off-by: github-actions[bot] # Conflicts: # CHANGELOG.md Co-authored-by: github-actions[bot] --- .../dashboards-assistant.release-notes-2.16.0.0.md | 6 +++++- server/routes/get_agent.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/release-notes/dashboards-assistant.release-notes-2.16.0.0.md b/release-notes/dashboards-assistant.release-notes-2.16.0.0.md index c9804b9f..4c7315db 100644 --- a/release-notes/dashboards-assistant.release-notes-2.16.0.0.md +++ b/release-notes/dashboards-assistant.release-notes-2.16.0.0.md @@ -4,4 +4,8 @@ Compatible with OpenSearch and OpenSearch Dashboards Version 2.16.0 ### Features -- Add feature to support text to visualization. ([#218](https://github.com/opensearch-project/dashboards-assistant/pull/218)) \ No newline at end of file +- Add feature to support text to visualization. ([#218](https://github.com/opensearch-project/dashboards-assistant/pull/218)) + +### Maintenance + +- Make ML Configuration Index Mapping be compatible with ml-commons plugin. ([#239](https://github.com/opensearch-project/dashboards-assistant/pull/239)) diff --git a/server/routes/get_agent.ts b/server/routes/get_agent.ts index a8d285a6..76cdd467 100644 --- a/server/routes/get_agent.ts +++ b/server/routes/get_agent.ts @@ -14,10 +14,13 @@ export const getAgent = async (id: string, client: OpenSearchClient['transport'] path, }); - if (!response || !response.body.configuration?.agent_id) { + if ( + !response || + !(response.body.ml_configuration?.agent_id || response.body.configuration?.agent_id) + ) { throw new Error(`cannot get agent ${id} by calling the api: ${path}`); } - return response.body.configuration.agent_id; + return response.body.ml_configuration?.agent_id || response.body.configuration.agent_id; } catch (error) { const errorMessage = JSON.stringify(error.meta?.body) || error; throw new Error(`get agent ${id} failed, reason: ${errorMessage}`); From 32c099ee347f05de5f6c069d42dec332521bb800 Mon Sep 17 00:00:00 2001 From: Hailong Cui Date: Mon, 29 Jul 2024 18:14:04 +0800 Subject: [PATCH 08/10] [Manual backport] [Backport 2.16] build(deps): bump ws from 8.16.0 to 8.18.0 (#244) * Merge pull request #221 from opensearch-project/dependabot/npm_and_yarn/ws-8.18.0 build(deps): bump ws from 8.16.0 to 8.18.0 (cherry picked from commit beac8cce7e4e69ca945ed249965ff8e25d6d4859) Signed-off-by: Cui Hailong * changelog Signed-off-by: Cui Hailong --------- Signed-off-by: Cui Hailong Co-authored-by: Xuesong Luo --- CHANGELOG.md | 3 ++- yarn.lock | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91f09f4e..9d035c63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,4 +8,5 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Change implementation of basic_input_output to built-in parser ([#10](https://github.com/opensearch-project/dashboards-assistant/pull/10)) - Add interactions into ChatState and pass specific interaction into message_bubble ([#12](https://github.com/opensearch-project/dashboards-assistant/pull/12)) - Refactor the code to get root agent id by calling the API in ml-commons plugin ([#128](https://github.com/opensearch-project/dashboards-assistant/pull/128)) -- build(deps): bump braces from 3.0.2 to 3.0.3 ([#213](https://github.com/opensearch-project/dashboards-assistant/pull/213)) \ No newline at end of file +- build(deps): bump braces from 3.0.2 to 3.0.3 ([#213](https://github.com/opensearch-project/dashboards-assistant/pull/213)) +- build(deps): bump ws from 8.16.0 to 8.18.0 ([#221](https://github.com/opensearch-project/dashboards-assistant/pull/221)) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 187f379f..9473664f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,9 +1746,9 @@ write@1.0.3: mkdirp "^0.5.1" ws@^8.13.0: - version "8.16.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" - integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== xml-name-validator@^4.0.0: version "4.0.0" From 8181e88edd1d2e3de5d28d7709d78264b30a218f Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:18:52 +0800 Subject: [PATCH 09/10] feat: fix ci (#246) (#248) (cherry picked from commit d94b8967ffc6a26b20664995f56fb5e70696c170) Signed-off-by: SuZhou-Joe Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- public/components/core_visualization.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/components/core_visualization.test.tsx b/public/components/core_visualization.test.tsx index e6831c42..f0663d73 100644 --- a/public/components/core_visualization.test.tsx +++ b/public/components/core_visualization.test.tsx @@ -9,6 +9,10 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import * as coreContextExports from '../contexts/core_context'; import { CoreVisualization } from './core_visualization'; +jest.mock('../../../../src/plugins/embeddable/public', () => ({ + ViewMode: jest.requireActual('../../../../src/plugins/embeddable/public/lib').ViewMode, +})); + describe('', () => { beforeEach(() => { jest.spyOn(coreContextExports, 'useCore').mockReturnValue({ From fa0e7d2bb04008c2e9ded6d7f993cff89b3154a9 Mon Sep 17 00:00:00 2001 From: opensearch-ci-bot Date: Mon, 19 Aug 2024 00:22:09 +0000 Subject: [PATCH 10/10] Increment version to 2.16.0.0 Signed-off-by: opensearch-ci-bot --- opensearch_dashboards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opensearch_dashboards.json b/opensearch_dashboards.json index 67323557..fbea23ae 100644 --- a/opensearch_dashboards.json +++ b/opensearch_dashboards.json @@ -21,4 +21,4 @@ "configPath": [ "assistant" ] -} +} \ No newline at end of file