From 958ea970285688c77a455dac4189925067d5c106 Mon Sep 17 00:00:00 2001 From: Heng Qian Date: Tue, 23 Jul 2024 11:48:18 +0800 Subject: [PATCH] Address comments and add UTs Signed-off-by: Heng Qian --- server/parsers/ParseHelper.test.ts | 124 ++++++++++++++++++ server/parsers/ParserHelper.ts | 24 ++-- .../parsers/basic_input_output_parser.test.ts | 42 ++++++ server/parsers/basic_input_output_parser.ts | 9 +- server/parsers/create_monitor_parser.ts | 55 -------- 5 files changed, 184 insertions(+), 70 deletions(-) create mode 100644 server/parsers/ParseHelper.test.ts delete mode 100644 server/parsers/create_monitor_parser.ts diff --git a/server/parsers/ParseHelper.test.ts b/server/parsers/ParseHelper.test.ts new file mode 100644 index 00000000..76b715dd --- /dev/null +++ b/server/parsers/ParseHelper.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { parseAdditionalActions } from './ParserHelper'; + +describe('ParseHelper', () => { + it('return additional actions when there is CreateAlertTool.output', async () => { + const output: string = + '\n' + + '{\n' + + ' "name": "Flight Delay Alert",\n' + + ' "search": {\n' + + ' "indices": ["opensearch_dashboards_sample_data_flights"],\n' + + ' "timeField": "timestamp",\n' + + ' "bucketValue": 12,\n' + + ' "bucketUnitOfTime": "h",\n' + + ' "filters": [\n' + + ' {\n' + + ' "fieldName": [\n' + + ' {\n' + + ' "label": "FlightDelayMin",\n' + + ' "type": "integer"\n' + + ' }\n' + + ' ],\n' + + ' "fieldValue": 0,\n' + + ' "operator": "is_greater"\n' + + ' }\n' + + ' ],\n' + + ' "aggregations": [\n' + + ' {\n' + + ' "aggregationType": "sum",\n' + + ' "fieldName": "FlightDelayMin"\n' + + ' }\n' + + ' ]\n' + + ' },\n' + + ' "triggers": [\n' + + ' {\n' + + ' "name": "Delayed Time Exceeds 1000 Minutes",\n' + + ' "severity": 2,\n' + + ' "thresholdValue": 1000,\n' + + ' "thresholdEnum": "ABOVE"\n' + + ' }\n' + + ' ]\n' + + '}\n'; + const expectedContent: string = + 'name=Flight%20Delay%20Alert&index=opensearch_dashboards_sample_data_flights&timeField=timestamp&bucketValue=12&bucketUnitOfTime=h&filters=%5B%7B%22fieldName%22%3A%5B%7B%22label%22%3A%22FlightDelayMin%22%2C%22type%22%3A%22integer%22%7D%5D%2C%22fieldValue%22%3A0%2C%22operator%22%3A%22is_greater%22%7D%5D&aggregations=%5B%7B%22aggregationType%22%3A%22sum%22%2C%22fieldName%22%3A%22FlightDelayMin%22%7D%5D&triggers=%5B%7B%22name%22%3A%22Delayed%20Time%20Exceeds%201000%20Minutes%22%2C%22severity%22%3A2%2C%22thresholdValue%22%3A1000%2C%22thresholdEnum%22%3A%22ABOVE%22%7D%5D'; + expect( + parseAdditionalActions({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'CreateAlertTool.output': [output], + }, + }) + ).toEqual([ + { + actionType: 'create_alert_button', + message: 'Create Alert', + content: expectedContent, + }, + ]); + }); + + it('do not return additional actions when CreateAlertTool.output is null', async () => { + expect( + parseAdditionalActions({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: {}, + }) + ).toEqual([]); + }); + + // Normally It won't happen since backend will never put a non-json output inCreateAlertTool.output. But we still want to handle it and add `create alert` button as additional action as it has invoked create alert tool. + it('return additional actions with empty content when CreateAlertTool.output is wrong format', async () => { + expect( + parseAdditionalActions({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'CreateAlertTool.output': ['None json output'], + }, + }) + ).toEqual([ + { + actionType: 'create_alert_button', + message: 'Create Alert', + content: '', + }, + ]); + }); + + it('return additional actions with existing info when CreateAlertTool.output missing part of parameters', async () => { + expect( + parseAdditionalActions({ + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'CreateAlertTool.output': ['{"name": "Test name"}'], + }, + }) + ).toEqual([ + { + actionType: 'create_alert_button', + message: 'Create Alert', + content: 'name=Test%20name&triggers=', + }, + ]); + }); +}); diff --git a/server/parsers/ParserHelper.ts b/server/parsers/ParserHelper.ts index 962c42bc..3b8ad2da 100644 --- a/server/parsers/ParserHelper.ts +++ b/server/parsers/ParserHelper.ts @@ -10,21 +10,27 @@ import { Interaction, } from '../../common/types/chat_saved_object_attributes'; -export const CreateMonitorParserHelper = (interaction: Interaction): IAdditionalAction[] => { +/* + * Add additional actions following the basic output in the same message bubble. + * Currently, only CreateAlertTool.output will add additional action, may be extended in the future. + */ +export const parseAdditionalActions = (interaction: Interaction): IAdditionalAction[] => { const monitorParameters = - (interaction.additional_info?.['CreateAlertTool.output'] as string[] | null)?.flatMap( + (interaction.additional_info?.['CreateAlertTool.output'] as string[] | null)?.map( (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); + if (parsedItem.search) { + 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 = {}; @@ -36,7 +42,7 @@ export const CreateMonitorParserHelper = (interaction: Interaction): IAdditional if (!monitorParameters.length) return []; - return [...new Set(monitorParameters)] + return monitorParameters .filter((parameters) => parameters) .map((parameters) => ({ actionType: 'create_alert_button', diff --git a/server/parsers/basic_input_output_parser.test.ts b/server/parsers/basic_input_output_parser.test.ts index 028ac81b..1f6b57dd 100644 --- a/server/parsers/basic_input_output_parser.test.ts +++ b/server/parsers/basic_input_output_parser.test.ts @@ -28,6 +28,7 @@ describe('BasicInputOutputParser', () => { type: 'output', contentType: 'markdown', content: 'response', + additionalActions: [], interactionId: 'interaction_id', suggestedActions: [], }, @@ -59,6 +60,7 @@ describe('BasicInputOutputParser', () => { type: 'output', contentType: 'markdown', content: 'response', + additionalActions: [], interactionId: 'interaction_id', suggestedActions: [ { @@ -74,6 +76,44 @@ describe('BasicInputOutputParser', () => { ]); }); + it('return additional actions when additional_info has related info', async () => { + const item = { + input: 'input', + response: 'response', + conversation_id: '', + interaction_id: 'interaction_id', + create_time: '', + additional_info: { + 'CreateAlertTool.output': ['{"name": "Test name"}'], + }, + }; + expect( + await BasicInputOutputParser.parserProvider(item, { + interactions: [item], + }) + ).toEqual([ + { + type: 'input', + contentType: 'text', + content: 'input', + }, + { + type: 'output', + contentType: 'markdown', + content: 'response', + additionalActions: [ + { + actionType: 'create_alert_button', + message: 'Create Alert', + content: 'name=Test%20name&triggers=', + }, + ], + interactionId: 'interaction_id', + suggestedActions: [], + }, + ]); + }); + it("should only parse latest interaction's suggestions field", async () => { const item = { input: 'input', @@ -105,6 +145,7 @@ describe('BasicInputOutputParser', () => { type: 'output', contentType: 'markdown', content: 'response', + additionalActions: [], interactionId: 'interaction_id', suggestedActions: [], }, @@ -133,6 +174,7 @@ describe('BasicInputOutputParser', () => { { content: 'normal text [](http://evil.com/) [image](http://evil.com/) [good link](https://link)', + additionalActions: [], contentType: 'markdown', interactionId: 'interaction_id', type: 'output', diff --git a/server/parsers/basic_input_output_parser.ts b/server/parsers/basic_input_output_parser.ts index da0519c2..531d3d0a 100644 --- a/server/parsers/basic_input_output_parser.ts +++ b/server/parsers/basic_input_output_parser.ts @@ -7,7 +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'; +import { parseAdditionalActions } from './ParserHelper'; const sanitize = (content: string) => { const window = new JSDOM('').window; @@ -93,16 +93,13 @@ export const BasicInputOutputParser: MessageParser = { }; // 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 additionalActions = parseAdditionalActions(interaction); const outputItems: IOutput[] = [ { type: 'output', contentType: 'markdown', content: sanitize(interaction.response), - additionalActions: alertActions, + additionalActions, 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 deleted file mode 100644 index 7a3f6496..00000000 --- a/server/parsers/create_monitor_parser.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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; - }, -};