From 8b29989c88f30b91662c175c2e62f9ebee4cd90a Mon Sep 17 00:00:00 2001 From: Heng Qian Date: Tue, 16 Jul 2024 14:12:56 +0800 Subject: [PATCH] Support creating alert in the chat window Signed-off-by: Heng Qian --- common/types/chat_saved_object_attributes.ts | 15 +++- public/chat_flyout.tsx | 74 ++++++++++--------- public/chat_header_button.tsx | 24 +++++- public/contexts/chat_context.tsx | 4 + public/index.scss | 2 + public/tabs/chat/messages/message_content.tsx | 40 +++++++++- public/tabs/chat_override_header.tsx | 53 +++++++++++++ public/types.ts | 3 +- public/utils/constants.ts | 2 + server/parsers/ParserHelper.ts | 46 ++++++++++++ server/parsers/basic_input_output_parser.ts | 8 ++ server/parsers/create_monitor_parser.ts | 55 ++++++++++++++ 12 files changed, 284 insertions(+), 42 deletions(-) create mode 100644 public/tabs/chat_override_header.tsx create mode 100644 server/parsers/ParserHelper.ts create mode 100644 server/parsers/create_monitor_parser.ts diff --git a/common/types/chat_saved_object_attributes.ts b/common/types/chat_saved_object_attributes.ts index 5f18c51f..6984bca1 100644 --- a/common/types/chat_saved_object_attributes.ts +++ b/common/types/chat_saved_object_attributes.ts @@ -52,19 +52,22 @@ export interface IOutput { toolsUsed?: string[]; contentType: 'error' | 'markdown' | 'visualization' | string; content: string; + additionalActions?: IAdditionalAction[]; suggestedActions?: ISuggestedAction[]; messageId?: string; fullWidth?: boolean; } export type IMessage = IInput | IOutput; -interface ISuggestedActionBase { +interface IActionBase { actionType: string; message: string; } -export type ISuggestedAction = ISuggestedActionBase & +export type ISuggestedAction = IActionBase & ( - | { actionType: 'send_as_input' | 'copy' | 'view_in_dashboards' } + | { + actionType: 'send_as_input' | 'copy' | 'view_in_dashboards' | 'create_monitor_in_dashboard'; + } | { actionType: 'view_ppl_visualization'; metadata: { query: string; question: string }; @@ -74,6 +77,12 @@ export type ISuggestedAction = ISuggestedActionBase & metadata: { interactionId: string }; } ); + +export type IAdditionalAction = IActionBase & { + actionType: 'create_monitor_grid'; + content: string; +}; + export interface SendFeedbackBody { satisfaction: boolean; } diff --git a/public/chat_flyout.tsx b/public/chat_flyout.tsx index a4a76658..17a2f6f5 100644 --- a/public/chat_flyout.tsx +++ b/public/chat_flyout.tsx @@ -12,6 +12,7 @@ import { ChatWindowHeader } from './tabs/chat_window_header'; import { ChatHistoryPage } from './tabs/history/chat_history_page'; import { AgentFrameworkTracesFlyoutBody } from './components/agent_framework_traces_flyout_body'; import { TAB_ID } from './utils/constants'; +import { ChatOverrideHeader } from './tabs/chat_override_header'; interface ChatFlyoutProps { flyoutVisible: boolean; @@ -86,52 +87,55 @@ export const ChatFlyout = (props: ChatFlyoutProps) => { > <>
- + {props.overrideComponent ? : }
- {props.overrideComponent} - - {(Panel, Resizer) => ( - <> - - - + {props.overrideComponent ? ( + props.overrideComponent + ) : ( + + {(Panel, Resizer) => ( <> - {resizable && } - {chatHistoryPageLoadedRef.current && ( - - )} - {chatTraceVisible && chatContext.interactionId && ( - - )} + {} + <> + {resizable && } + + {chatHistoryPageLoadedRef.current && ( + + )} + {chatTraceVisible && chatContext.interactionId && ( + + )} + + - - )} - + )} + + )} ); diff --git a/public/chat_header_button.tsx b/public/chat_header_button.tsx index 2f83e388..03d7f5d0 100644 --- a/public/chat_header_button.tsx +++ b/public/chat_header_button.tsx @@ -18,12 +18,20 @@ import { ChatStateProvider } from './hooks'; import './index.scss'; import { ActionExecutor, AssistantActions, MessageRenderer, TabId, UserAccount } from './types'; import { - TAB_ID, DEFAULT_SIDECAR_DOCKED_MODE, DEFAULT_SIDECAR_LEFT_OR_RIGHT_SIZE, + OVERRIDE_SIDECAR_LEFT_OR_RIGHT_SIZE, + TAB_ID, } from './utils/constants'; import { useCore } from './contexts/core_context'; import { MountPointPortal } from '../../../src/plugins/opensearch_dashboards_react/public'; +import { getStateFromOsdUrl } from '../../../src/plugins/opensearch_dashboards_utils/public'; +import { + DiscoverRootState, + DiscoverState, +} from '../../../src/plugins/discover/public/application/utils/state_management'; +import { IndexPatternAttributes } from '../../../src/plugins/data/common'; +import { SavedSearch } from '../../../src/plugins/discover/public'; interface HeaderChatButtonProps { application: ApplicationStart; @@ -41,6 +49,7 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { const [conversationId, setConversationId] = useState(); const [title, setTitle] = useState(); const [flyoutVisible, setFlyoutVisible] = useState(false); + const [overrideName, setOverrideName] = useState(); const [flyoutComponent, setFlyoutComponent] = useState(null); const [selectedTabId, setSelectedTabId] = useState(TAB_ID.CHAT); const [preSelectedTabId, setPreSelectedTabId] = useState(undefined); @@ -74,6 +83,8 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { flyoutFullScreen, setFlyoutVisible, setFlyoutComponent, + overrideName, + setOverrideName, userHasAccess: props.userHasAccess, messageRenderers: props.messageRenderers, actionExecutors: props.actionExecutors, @@ -156,6 +167,17 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => { flyoutMountPoint.current = mountPoint; }, []); + useEffect(() => { + if (flyoutLoaded && flyoutVisible) { + core.overlays.sidecar().setSidecarConfig({ + paddingSize: + selectedTabId === TAB_ID.OVERRIDE + ? OVERRIDE_SIDECAR_LEFT_OR_RIGHT_SIZE + : DEFAULT_SIDECAR_LEFT_OR_RIGHT_SIZE, + }); + } + }, [selectedTabId === TAB_ID.OVERRIDE]); + useEffect(() => { if (!props.userHasAccess) { return; diff --git a/public/contexts/chat_context.tsx b/public/contexts/chat_context.tsx index f7bc516c..68053822 100644 --- a/public/contexts/chat_context.tsx +++ b/public/contexts/chat_context.tsx @@ -18,6 +18,8 @@ export interface IChatContext { flyoutFullScreen: boolean; setFlyoutVisible: React.Dispatch>; setFlyoutComponent: React.Dispatch>; + overrideName?: string; + setOverrideName: React.Dispatch>; userHasAccess: boolean; messageRenderers: Record; actionExecutors: Record; @@ -28,6 +30,8 @@ export interface IChatContext { setInteractionId: React.Dispatch>; sidecarDockedMode: ISidecarConfig['dockedMode']; setSidecarDockedMode: React.Dispatch>; + tabRenderer?: React.ReactNode; + setTabRenderer?: React.Dispatch>; } export const ChatContext = React.createContext(null); diff --git a/public/index.scss b/public/index.scss index 733b86f4..e9e4abd2 100644 --- a/public/index.scss +++ b/public/index.scss @@ -68,6 +68,8 @@ .llm-chat-flyout { height: 100%; + display: flex; + flex-direction: column; .euiFlyoutFooter { background: transparent; } diff --git a/public/tabs/chat/messages/message_content.tsx b/public/tabs/chat/messages/message_content.tsx index 5b0be173..90369229 100644 --- a/public/tabs/chat/messages/message_content.tsx +++ b/public/tabs/chat/messages/message_content.tsx @@ -3,11 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiMarkdownFormat, EuiText } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiMarkdownFormat, EuiText } from '@elastic/eui'; import React from 'react'; import { IMessage } from '../../../../common/types/chat_saved_object_attributes'; import { CoreVisualization } from '../../../components/core_visualization'; import { useChatContext } from '../../../contexts/chat_context'; +import { TAB_ID } from '../../../utils/constants'; export interface MessageContentProps { message: IMessage; @@ -28,7 +29,42 @@ export const MessageContent: React.FC = React.memo((props) ); case 'markdown': - return {props.message.content}; + return ( + <> + {props.message.content} + {props.message.additionalActions && + props.message.additionalActions.map((action, index) => ( + + + { + if (chatContext) { + if (chatContext.selectedTabId !== TAB_ID.OVERRIDE) { + chatContext.setSelectedTabId(TAB_ID.OVERRIDE); + } + const actionMessage: IMessage = { + type: 'output', + contentType: action.actionType, + content: action.content, + }; + const component = chatContext.messageRenderers[ + action.actionType + ]?.(actionMessage, { props: { message: actionMessage }, chatContext }); + chatContext.setFlyoutComponent(component); + chatContext.setOverrideName('Create Alert'); + } + }} + fill + isLoading={false} + disabled={false} + > + Create monitor + + + + ))} + + ); case 'visualization': return ( diff --git a/public/tabs/chat_override_header.tsx b/public/tabs/chat_override_header.tsx new file mode 100644 index 00000000..b9a92871 --- /dev/null +++ b/public/tabs/chat_override_header.tsx @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiButtonEmpty, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { IChatContext, useChatContext } from '../contexts/chat_context'; +import { TAB_ID } from '../utils/constants'; +import { SidecarIconMenu } from '../components/sidecar_icon_menu'; + +export const ChatOverrideHeader = React.memo(() => { + const chatContext = useChatContext() as IChatContext; + const { setSelectedTabId, setFlyoutComponent, setOverrideName } = chatContext; + + const handleBack = useCallback(() => { + setSelectedTabId(TAB_ID.CHAT); + setFlyoutComponent(null); + setOverrideName(undefined); + }, [setSelectedTabId]); + + return ( + <> + + + + + {chatContext?.overrideName || 'Back'} + + + + + + { + chatContext.setFlyoutVisible(false); + }} + /> + + + + + ); +}); diff --git a/public/types.ts b/public/types.ts index f4bde66f..c88ccf51 100644 --- a/public/types.ts +++ b/public/types.ts @@ -9,6 +9,7 @@ import { IMessage, ISuggestedAction } from '../common/types/chat_saved_object_at import { IChatContext } from './contexts/chat_context'; import { MessageContentProps } from './tabs/chat/messages/message_content'; import { IncontextInsightRegistry } from './services'; +import { TAB_ID } from './utils/constants'; export interface RenderProps { props: MessageContentProps; @@ -83,4 +84,4 @@ export type IncontextInsightType = | 'chatWithSuggestions' | 'error'; -export type TabId = 'chat' | 'compose' | 'insights' | 'history' | 'trace'; +export type TabId = TAB_ID; diff --git a/public/utils/constants.ts b/public/utils/constants.ts index 44978b4c..50025af5 100644 --- a/public/utils/constants.ts +++ b/public/utils/constants.ts @@ -10,9 +10,11 @@ export enum TAB_ID { INSIGHTS = 'insights', HISTORY = 'history', TRACE = 'trace', + OVERRIDE = 'override', } export const DEFAULT_SIDECAR_DOCKED_MODE = SIDECAR_DOCKED_MODE.RIGHT; export const DEFAULT_SIDECAR_LEFT_OR_RIGHT_SIZE = 460; +export const OVERRIDE_SIDECAR_LEFT_OR_RIGHT_SIZE = 570; // this is a default padding top size for sidecar when switching to takeover export const DEFAULT_SIDECAR_TAKEOVER_PADDING_TOP_SIZE = 136; diff --git a/server/parsers/ParserHelper.ts b/server/parsers/ParserHelper.ts new file mode 100644 index 00000000..ef502dce --- /dev/null +++ b/server/parsers/ParserHelper.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Qs from 'querystring'; +import { + IAdditionalAction, + IMessage, + Interaction, +} from '../../common/types/chat_saved_object_attributes'; + +export const CreateMonitorParserHelper = (interaction: Interaction): IAdditionalAction[] => { + const monitorParameters = + (interaction.additional_info?.['CreateAlertTool.output'] as string[] | null)?.flatMap( + (item: string): {} => { + // @typescript-eslint/no-explicit-any + let parameters: { [key: string]: string } = {}; + try { + const parsedItem = JSON.parse(item); + parameters.name = parsedItem.name; + parameters.index = parsedItem.search.indices; + parameters.timeField = parsedItem.search.timeField; + parameters.bucketValue = parsedItem.search.bucketValue; + parameters.bucketUnitOfTime = parsedItem.search.bucketUnitOfTime; + parameters.filters = JSON.stringify(parsedItem.search.filters); + parameters.aggregations = JSON.stringify(parsedItem.search.aggregations); + parameters.triggers = JSON.stringify(parsedItem.triggers); + } catch (e) { + parameters = {}; + } + + return parameters; + } + ) || []; + + if (!monitorParameters.length) return []; + + return [...new Set(monitorParameters)] + .filter((parameters) => parameters) + .map((parameters) => ({ + actionType: 'create_monitor_grid', + message: 'Create Alert', + content: Qs.stringify(parameters), + })); +}; diff --git a/server/parsers/basic_input_output_parser.ts b/server/parsers/basic_input_output_parser.ts index 257ba12e..da0519c2 100644 --- a/server/parsers/basic_input_output_parser.ts +++ b/server/parsers/basic_input_output_parser.ts @@ -7,6 +7,7 @@ import createDOMPurify from 'dompurify'; import { JSDOM } from 'jsdom'; import { IInput, IOutput } from '../../common/types/chat_saved_object_attributes'; import { MessageParser } from '../types'; +import { CreateMonitorParserHelper } from './ParserHelper'; const sanitize = (content: string) => { const window = new JSDOM('').window; @@ -90,11 +91,18 @@ export const BasicInputOutputParser: MessageParser = { contentType: 'text', content: interaction.input, }; + + // TODO: make it more general by using registration of all internal parsers. + const alertActions = + interaction.additional_info && 'CreateAlertTool.output' in interaction.additional_info + ? CreateMonitorParserHelper(interaction) + : undefined; const outputItems: IOutput[] = [ { type: 'output', contentType: 'markdown', content: sanitize(interaction.response), + additionalActions: alertActions, interactionId: interaction.interaction_id, suggestedActions: suggestedActions .filter((item) => item) diff --git a/server/parsers/create_monitor_parser.ts b/server/parsers/create_monitor_parser.ts new file mode 100644 index 00000000..7a3f6496 --- /dev/null +++ b/server/parsers/create_monitor_parser.ts @@ -0,0 +1,55 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import Qs from 'querystring'; +import { IMessage } from '../../common/types/chat_saved_object_attributes'; +import { MessageParser } from '../types'; + +export const CreateMonitorParsers: MessageParser = { + id: 'create_monitor_message', + async parserProvider(interaction) { + const monitorParameters = + (interaction.additional_info?.['CreateAlertTool.output'] as string[] | null)?.flatMap( + (item: string): {} => { + // @typescript-eslint/no-explicit-any + let parameters: { [key: string]: string } = {}; + try { + const parsedItem = JSON.parse(item); + parameters.name = parsedItem.name; + parameters.index = parsedItem.search.indices; + parameters.timeField = parsedItem.search.timeField; + parameters.bucketValue = parsedItem.search.bucketValue; + parameters.bucketUnitOfTime = parsedItem.search.bucketUnitOfTime; + parameters.filters = JSON.stringify(parsedItem.search.filters); + parameters.aggregations = JSON.stringify(parsedItem.search.aggregations); + parameters.triggers = JSON.stringify(parsedItem.triggers); + } catch (e) { + parameters = {}; + } + + return parameters; + } + ) || []; + + if (!monitorParameters.length) return []; + + const createMonitorOutputs: IMessage[] = [...new Set(monitorParameters)] + .filter((parameters) => parameters) + .map((parameters) => ({ + type: 'output', + content: Qs.stringify(parameters), + contentType: 'create_monitor_grid', + fullWidth: true, + suggestedActions: [ + { + message: 'Create alert with AI suggested parameters in alerting page.', + actionType: 'create_monitor_in_dashboard', + }, + ], + })); + + return createMonitorOutputs; + }, +};