diff --git a/CHANGELOG.md b/CHANGELOG.md index 93747cd3..aa57c78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,4 +30,5 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Add experimental feature to support text to visualization ([#218](https://github.com/opensearch-project/dashboards-assistant/pull/218)) - Be compatible with ML configuration index mapping change ([#239](https://github.com/opensearch-project/dashboards-assistant/pull/239)) - Support context aware alert analysis by reusing incontext insight component([#215](https://github.com/opensearch-project/dashboards-assistant/pull/215)) -- Use smaller and compressed variants of buttons and form components ([#250](https://github.com/opensearch-project/dashboards-assistant/pull/250)) \ No newline at end of file +- Use smaller and compressed variants of buttons and form components ([#250](https://github.com/opensearch-project/dashboards-assistant/pull/250)) +- Support insight with RAG in alert analysis assistant and refine the UX ([#266](https://github.com/opensearch-project/dashboards-assistant/pull/266)) diff --git a/common/constants/llm.ts b/common/constants/llm.ts index f2839d75..21bb0ab4 100644 --- a/common/constants/llm.ts +++ b/common/constants/llm.ts @@ -28,6 +28,11 @@ export const AGENT_API = { EXECUTE: `${API_BASE}/agent/_execute`, }; +export const SUMMARY_ASSISTANT_API = { + SUMMARIZE: `${API_BASE}/summary`, + INSIGHT: `${API_BASE}/insight`, +}; + export const NOTEBOOK_API = { CREATE_NOTEBOOK: `${NOTEBOOK_PREFIX}/note`, SET_PARAGRAPH: `${NOTEBOOK_PREFIX}/set_paragraphs/`, diff --git a/public/assets/shiny_sparkle.svg b/public/assets/shiny_sparkle.svg new file mode 100644 index 00000000..1aeadce0 --- /dev/null +++ b/public/assets/shiny_sparkle.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/sparkle.svg b/public/assets/sparkle.svg new file mode 100644 index 00000000..885e5c63 --- /dev/null +++ b/public/assets/sparkle.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/components/incontext_insight/generate_popover_body.test.tsx b/public/components/incontext_insight/generate_popover_body.test.tsx index 5001e1c7..3fb3c3e7 100644 --- a/public/components/incontext_insight/generate_popover_body.test.tsx +++ b/public/components/incontext_insight/generate_popover_body.test.tsx @@ -8,7 +8,7 @@ import { render, cleanup, fireEvent, waitFor } from '@testing-library/react'; import { getConfigSchema, getNotifications } from '../../services'; import { GeneratePopoverBody } from './generate_popover_body'; import { HttpSetup } from '../../../../../src/core/public'; -import { ASSISTANT_API } from '../../../common/constants/llm'; +import { SUMMARY_ASSISTANT_API } from '../../../common/constants/llm'; jest.mock('../../services'); @@ -37,30 +37,33 @@ describe('GeneratePopoverBody', () => { contextProvider: jest.fn(), suggestions: ['Test summarization question'], datasourceId: 'test-datasource', - key: 'test-key', + key: 'alerts', }; const closePopoverMock = jest.fn(); - it('renders the generate summary button', () => { - const { getByText } = render( - - ); - - expect(getByText('Generate summary')).toBeInTheDocument(); - }); - - it('calls onGenerateSummary when button is clicked', async () => { - mockPost.mockResolvedValue({ - interactions: [{ conversation_id: 'test-conversation' }], - messages: [{ type: 'output', content: 'Generated summary content' }], + it('auto generates summary and insight', async () => { + mockPost.mockImplementation((path: string, body) => { + let value; + switch (path) { + case SUMMARY_ASSISTANT_API.SUMMARIZE: + value = { + summary: 'Generated summary content', + insightAgentIdExists: true, + }; + break; + + case SUMMARY_ASSISTANT_API.INSIGHT: + value = 'Generated insight content'; + break; + + default: + return null; + } + return Promise.resolve(value); }); - const { getByText } = render( + const { getByText, getByLabelText, queryByText, queryByLabelText } = render( { /> ); - const button = getByText('Generate summary'); - fireEvent.click(button); + // 1. Auto generate summary + // title is assistant icon + 'Summary' + expect(getByLabelText('alert-assistant')).toBeInTheDocument(); + expect(getByText('Summary')).toBeInTheDocument(); + // content is loading + expect(getByLabelText('loading_content')).toBeInTheDocument(); // Wait for loading to complete and summary to render await waitFor(() => { expect(getByText('Generated summary content')).toBeInTheDocument(); }); - - expect(mockPost).toHaveBeenCalledWith(ASSISTANT_API.SEND_MESSAGE, expect.any(Object)); + // loading content disappeared + expect(queryByLabelText('loading_content')).toBeNull(); + expect(mockPost).toHaveBeenCalledWith(SUMMARY_ASSISTANT_API.SUMMARIZE, expect.any(Object)); expect(mockToasts.addDanger).not.toHaveBeenCalled(); - }); - - it('shows loading state while generating summary', async () => { - const { getByText } = render( - - ); - const button = getByText('Generate summary'); - fireEvent.click(button); + // insight tip icon is visible + const insightTipIcon = getByLabelText('Insight'); + expect(insightTipIcon).toBeInTheDocument(); - // Wait for loading state to appear - expect(getByText('Generating summary...')).toBeInTheDocument(); - }); - - it('handles error during summary generation', async () => { - mockPost.mockRejectedValue(new Error('Network Error')); - - const { getByText } = render( - - ); - - const button = getByText('Generate summary'); - fireEvent.click(button); + // 2. Click insight tip icon to view insight + fireEvent.click(insightTipIcon); + // title is back button + 'Insight With RAG' + let backButton = getByLabelText('back-to-summary'); + expect(backButton).toBeInTheDocument(); + expect(getByText('Insight With RAG')).toBeInTheDocument(); + // Wait for loading to complete and insight to render await waitFor(() => { - expect(mockToasts.addDanger).toHaveBeenCalledWith('Generate summary error'); + expect(getByText('Generated insight content')).toBeInTheDocument(); }); + expect(queryByText('Generated summary content')).toBeNull(); + + // loading content disappeared + expect(queryByLabelText('loading_content')).toBeNull(); + expect(mockPost).toHaveBeenCalledWith(SUMMARY_ASSISTANT_API.INSIGHT, expect.any(Object)); + expect(mockToasts.addDanger).not.toHaveBeenCalled(); + + // 3. Click back button to view summary + backButton = getByLabelText('back-to-summary'); + fireEvent.click(backButton); + expect(queryByText('Generated insight content')).toBeNull(); + expect(queryByText('Generated summary content')).toBeInTheDocument(); }); - it('renders the continue in chat button after summary is generated', async () => { - mockPost.mockResolvedValue({ - interactions: [{ conversation_id: 'test-conversation' }], - messages: [{ type: 'output', content: 'Generated summary content' }], + it('auto generates summary without insight agent id', async () => { + mockPost.mockImplementation((path: string, body) => { + let value; + switch (path) { + case SUMMARY_ASSISTANT_API.SUMMARIZE: + value = { + summary: 'Generated summary content', + insightAgentIdExists: false, + }; + break; + + case SUMMARY_ASSISTANT_API.INSIGHT: + value = 'Generated insight content'; + break; + + default: + return null; + } + return Promise.resolve(value); }); - const { getByText } = render( + const { getByText, getByLabelText, queryByLabelText } = render( { /> ); - const button = getByText('Generate summary'); - fireEvent.click(button); + // title is assistant icon + 'Summary' + expect(getByLabelText('alert-assistant')).toBeInTheDocument(); + expect(getByText('Summary')).toBeInTheDocument(); + // content is loading + expect(getByLabelText('loading_content')).toBeInTheDocument(); - // Wait for the summary to be displayed + // Wait for loading to complete and summary to render await waitFor(() => { expect(getByText('Generated summary content')).toBeInTheDocument(); }); + // loading content disappeared + expect(queryByLabelText('loading_content')).toBeNull(); + expect(mockPost).toHaveBeenCalledWith(SUMMARY_ASSISTANT_API.SUMMARIZE, expect.any(Object)); + expect(mockToasts.addDanger).not.toHaveBeenCalled(); - // Check for continue in chat button - expect(getByText('Continue in chat')).toBeInTheDocument(); + // insight tip icon is not visible + expect(queryByLabelText('Insight')).toBeNull(); + // Only call http post 1 time. + expect(mockPost).toHaveBeenCalledTimes(1); }); - it('calls onChatContinuation when continue in chat button is clicked', async () => { - mockPost.mockResolvedValue({ - interactions: [{ conversation_id: 'test-conversation' }], - messages: [{ type: 'output', content: 'Generated summary content' }], - }); + it('handles error during summary generation', async () => { + mockPost.mockRejectedValue(new Error('Network Error')); - const { getByText } = render( + const { queryByText } = render( ); - const button = getByText('Generate summary'); - fireEvent.click(button); + // Auto close popover window if error occurs + expect(queryByText('test-generated-popover')).toBeNull(); await waitFor(() => { - expect(getByText('Generated summary content')).toBeInTheDocument(); + expect(mockToasts.addDanger).toHaveBeenCalledWith('Generate summary error'); }); - - const continueButton = getByText('Continue in chat'); - fireEvent.click(continueButton); - - expect(mockPost).toHaveBeenCalledTimes(1); - expect(closePopoverMock).toHaveBeenCalled(); }); - it("continue in chat button doesn't appear when chat is disabled", async () => { - mockPost.mockResolvedValue({ - interactions: [{ conversation_id: 'test-conversation' }], - messages: [{ type: 'output', content: 'Generated summary content' }], - }); - (getConfigSchema as jest.Mock).mockReturnValue({ - chat: { enabled: false }, + it('handles error during insight generation', async () => { + mockPost.mockImplementation((path: string, body) => { + let value; + switch (path) { + case SUMMARY_ASSISTANT_API.SUMMARIZE: + value = { + summary: 'Generated summary content', + insightAgentIdExists: true, + }; + break; + + case SUMMARY_ASSISTANT_API.INSIGHT: + return Promise.reject(new Error('Network Error')); + + default: + return null; + } + return Promise.resolve(value); }); - const { getByText, queryByText } = render( + const { getByText, queryByLabelText } = render( ); - const button = getByText('Generate summary'); - fireEvent.click(button); - + expect(getByText('Summary')).toBeInTheDocument(); + // Wait for loading to complete and summary to render await waitFor(() => { - expect(getByText('Generated summary content')).toBeInTheDocument(); + expect(mockToasts.addDanger).toHaveBeenCalledWith('Generate insight error'); }); - - expect(queryByText('Continue in chat')).toBeNull(); - expect(mockPost).toHaveBeenCalledTimes(1); + // Show summary content although insight generation failed + expect(getByText('Generated summary content')).toBeInTheDocument(); + // insight tip icon is not visible for this alert + expect(queryByLabelText('Insight')).toBeNull(); }); }); diff --git a/public/components/incontext_insight/generate_popover_body.tsx b/public/components/incontext_insight/generate_popover_body.tsx index 2a9db358..fca2c10a 100644 --- a/public/components/incontext_insight/generate_popover_body.tsx +++ b/public/components/incontext_insight/generate_popover_body.tsx @@ -6,74 +6,97 @@ import React, { useState } from 'react'; import { i18n } from '@osd/i18n'; import { - EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiIconTip, + EuiLoadingContent, + EuiMarkdownFormat, EuiPanel, + EuiPopoverFooter, + EuiPopoverTitle, EuiSpacer, EuiText, + EuiTitle, } from '@elastic/eui'; +import { useEffectOnce } from 'react-use'; import { IncontextInsight as IncontextInsightInput } from '../../types'; -import { getConfigSchema, getIncontextInsightRegistry, getNotifications } from '../../services'; +import { getNotifications } from '../../services'; import { HttpSetup } from '../../../../../src/core/public'; -import { ASSISTANT_API } from '../../../common/constants/llm'; -import { getAssistantRole } from '../../utils/constants'; +import { SUMMARY_ASSISTANT_API } from '../../../common/constants/llm'; +import shiny_sparkle from '../../assets/shiny_sparkle.svg'; export const GeneratePopoverBody: React.FC<{ incontextInsight: IncontextInsightInput; httpSetup?: HttpSetup; closePopover: () => void; }> = ({ incontextInsight, httpSetup, closePopover }) => { - const [isLoading, setIsLoading] = useState(false); const [summary, setSummary] = useState(''); - const [conversationId, setConversationId] = useState(''); + const [insight, setInsight] = useState(''); + const [insightAvailable, setInsightAvailable] = useState(false); + const [showInsight, setShowInsight] = useState(false); const toasts = getNotifications().toasts; - const registry = getIncontextInsightRegistry(); - const onChatContinuation = () => { - registry?.continueInChat(incontextInsight, conversationId); - closePopover(); - }; + useEffectOnce(() => { + onGenerateSummary( + incontextInsight.suggestions && incontextInsight.suggestions.length > 0 + ? incontextInsight.suggestions[0] + : 'Please summarize the input' + ); + }); const onGenerateSummary = (summarizationQuestion: string) => { - setIsLoading(true); - setSummary(''); - setConversationId(''); const summarize = async () => { - const contextContent = incontextInsight.contextProvider - ? await incontextInsight.contextProvider() - : ''; - let incontextInsightType: string; + let contextObj; + try { + contextObj = (await incontextInsight.contextProvider?.()) ?? undefined; + } catch (e) { + console.error('Error executing contextProvider:', e); + toasts.addDanger( + i18n.translate('assistantDashboards.incontextInsight.generateSummaryError', { + defaultMessage: 'Generate summary error', + }) + ); + closePopover(); + return; + } + const contextContent = contextObj?.context || ''; + let summaryType: string; const endIndex = incontextInsight.key.indexOf('_', 0); if (endIndex !== -1) { - incontextInsightType = incontextInsight.key.substring(0, endIndex); + summaryType = incontextInsight.key.substring(0, endIndex); } else { - incontextInsightType = incontextInsight.key; + summaryType = incontextInsight.key; } + const insightType = + summaryType === 'alerts' + ? contextObj?.additionalInfo.monitorType === 'cluster_metrics_monitor' + ? 'os_insight' + : 'user_insight' + : undefined; await httpSetup - ?.post(ASSISTANT_API.SEND_MESSAGE, { + ?.post(SUMMARY_ASSISTANT_API.SUMMARIZE, { body: JSON.stringify({ - messages: [], - input: { - type: 'input', - content: summarizationQuestion, - contentType: 'text', - context: { content: contextContent, dataSourceId: incontextInsight.datasourceId }, - promptPrefix: getAssistantRole(incontextInsightType), - }, + type: summaryType, + insightType, + question: summarizationQuestion, + context: contextContent, }), }) .then((response) => { - const interactionLength = response.interactions.length; - if (interactionLength > 0) { - setConversationId(response.interactions[interactionLength - 1].conversation_id); - } - - const messageLength = response.messages.length; - if (messageLength > 0 && response.messages[messageLength - 1].type === 'output') { - setSummary(response.messages[messageLength - 1].content); + const summaryContent = response.summary; + setSummary(summaryContent); + const insightAgentIdExists = insightType !== undefined && response.insightAgentIdExists; + setInsightAvailable(insightAgentIdExists); + if (insightAgentIdExists) { + onGenerateInsightBasedOnSummary( + summaryType, + insightType, + summaryContent, + contextContent, + `Please provide your insight on this ${summaryType}.` + ); } }) .catch((error) => { @@ -82,65 +105,128 @@ export const GeneratePopoverBody: React.FC<{ defaultMessage: 'Generate summary error', }) ); - }) - .finally(() => { - setIsLoading(false); + closePopover(); }); }; return summarize(); }; - return summary ? ( - <> - - {summary} - - - {getConfigSchema().chat.enabled && ( - onChatContinuation()} - grow={false} - paddingSize="none" - style={{ width: '120px', float: 'right' }} - > - - - - - - - {i18n.translate('assistantDashboards.incontextInsight.continueInChat', { - defaultMessage: 'Continue in chat', - })} - - - + const onGenerateInsightBasedOnSummary = ( + summaryType: string, + insightType: string, + summaryContent: string, + context: string, + insightQuestion: string + ) => { + const generateInsight = async () => { + httpSetup + ?.post(SUMMARY_ASSISTANT_API.INSIGHT, { + body: JSON.stringify({ + summaryType, + insightType, + summary: summaryContent, + context, + question: insightQuestion, + }), + }) + .then((response) => { + setInsight(response); + }) + .catch((error) => { + toasts.addDanger( + i18n.translate('assistantDashboards.incontextInsight.generateSummaryError', { + defaultMessage: 'Generate insight error', + }) + ); + setInsightAvailable(false); + setShowInsight(false); + }); + }; + + return generateInsight(); + }; + + const renderContent = () => { + const content = showInsight && insightAvailable ? insight : summary; + return content ? ( + <> + + + {content} + + + {renderInnerFooter()} - )} + + ) : ( + + ); + }; + + const renderInnerTitle = () => { + return ( + + + + {showInsight ? ( + { + setShowInsight(false); + }} + type="arrowLeft" + color={'text'} + /> + ) : ( + + )} + + + + +
+ {i18n.translate('assistantDashboards.incontextInsight.Summary', { + defaultMessage: showInsight ? 'Insight With RAG' : 'Summary', + })} +
+
+
+
+
+
+ ); + }; + + const renderInnerFooter = () => { + return ( + + + {insightAvailable && ( + { + setShowInsight(true); + }} + > + + + )} + + + ); + }; + + return ( + <> + {renderInnerTitle()} + {renderContent()} - ) : ( - { - await onGenerateSummary( - incontextInsight.suggestions && incontextInsight.suggestions.length > 0 - ? incontextInsight.suggestions[0] - : 'Please summarize the input' - ); - }} - isLoading={isLoading} - disabled={isLoading} - > - {isLoading - ? i18n.translate('assistantDashboards.incontextInsight.generatingSummary', { - defaultMessage: 'Generating summary...', - }) - : i18n.translate('assistantDashboards.incontextInsight.generateSummary', { - defaultMessage: 'Generate summary', - })} - ); }; diff --git a/public/components/incontext_insight/index.scss b/public/components/incontext_insight/index.scss index 914f9b94..4c6ed7a5 100644 --- a/public/components/incontext_insight/index.scss +++ b/public/components/incontext_insight/index.scss @@ -107,10 +107,34 @@ } .incontextInsightPopoverBody { - max-width: 400px; + max-width: 486px; + min-width: 486px; width: 100%; } +.incontextInsightGeneratePopoverTitle { + // TODO: Remove this one paddingSize is fixed + padding: 0 !important; + padding-left: 5px !important; + margin-bottom: 0 !important; + min-height: 30px; + max-height: 30px; + border: none; +} + +.incontextInsightGeneratePopoverFooter{ + padding-block: 0 !important; + margin: 0 !important; + border: none; +} + +.incontextInsightGeneratePopoverContent { + display: flex; + overflow: auto; + max-height: 300px; + scrollbar-width: thin; +} + .incontextInsightSummary { border: $euiBorderThin; border-radius: 4px; diff --git a/public/components/incontext_insight/index.tsx b/public/components/incontext_insight/index.tsx index add28340..feaa87cf 100644 --- a/public/components/incontext_insight/index.tsx +++ b/public/components/incontext_insight/index.tsx @@ -7,29 +7,30 @@ import './index.scss'; import { i18n } from '@osd/i18n'; import { - EuiWrappingPopover, - EuiSmallButton, + EuiBadge, EuiCompressedFieldText, + EuiCompressedFormRow, EuiFlexGroup, EuiFlexItem, - EuiCompressedFormRow, - EuiPopoverTitle, - EuiText, - EuiPopoverFooter, - EuiBadge, - EuiSpacer, + EuiIcon, EuiListGroup, EuiListGroupItem, EuiPanel, - keys, - EuiIcon, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSmallButton, EuiSmallButtonIcon, + EuiSpacer, + EuiText, + EuiWrappingPopover, + keys, } from '@elastic/eui'; import React, { Children, isValidElement, useEffect, useRef, useState } from 'react'; import { IncontextInsight as IncontextInsightInput } from '../../types'; import { getIncontextInsightRegistry, getNotifications } from '../../services'; // TODO: Replace with getChrome().logos.Chat.url import chatIcon from '../../assets/chat.svg'; +import sparkle from '../../assets/sparkle.svg'; import { HttpSetup } from '../../../../../src/core/public'; import { GeneratePopoverBody } from './generate_popover_body'; @@ -260,7 +261,7 @@ export const IncontextInsight = ({ children, httpSetup }: IncontextInsightProps)
- +
@@ -305,37 +306,42 @@ export const IncontextInsight = ({ children, httpSetup }: IncontextInsightProps) offset={6} panelPaddingSize="s" > - - - -
- - {i18n.translate('assistantDashboards.incontextInsight.assistant', { - defaultMessage: 'OpenSearch Assistant', - })} - -
-
- -
- -
-
-
-
+ { + // For 'generate' type insights, we don't want to show this title but its own inner title + input.type !== 'generate' && ( + + + +
+ + {i18n.translate('assistantDashboards.incontextInsight.assistant', { + defaultMessage: 'OpenSearch Assistant', + })} + +
+
+ +
+ +
+
+
+
+ ) + }
{popoverBody()}
); diff --git a/public/types.ts b/public/types.ts index 0212dc40..32e521a5 100644 --- a/public/types.ts +++ b/public/types.ts @@ -104,13 +104,18 @@ export interface ChatConfig { export type IncontextInsights = Map; +export interface ContextObj { + context: string; + additionalInfo: Record; +} + export interface IncontextInsight { key: string; type?: IncontextInsightType; summary?: string; suggestions?: string[]; interactionId?: string; - contextProvider?: () => Promise; + contextProvider?: () => Promise; datasourceId?: string; } diff --git a/server/plugin.ts b/server/plugin.ts index b415392c..374d4ecd 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -19,6 +19,7 @@ import { registerChatRoutes } from './routes/chat_routes'; import { registerText2VizRoutes } from './routes/text2viz_routes'; import { AssistantService } from './services/assistant_service'; import { registerAgentRoutes } from './routes/agent_routes'; +import { registerSummaryAssistantRoutes } from './routes/summary_routes'; import { capabilitiesProvider } from './vis_type_nlq/capabilities_provider'; import { visNLQSavedObjectType } from './vis_type_nlq/saved_object_type'; @@ -60,6 +61,7 @@ export class AssistantPlugin implements Plugin { + try { + const requestParams = { + query: { + term: { + 'name.keyword': name, + }, + }, + _source: ['_id'], + sort: { + created_time: 'desc', + }, + size: 1, + }; + + const response = await client.request({ + method: 'GET', + path: `${ML_COMMONS_BASE_API}/agents/_search`, + body: requestParams, + }); + + if (!response || response.body.hits.total.value === 0) { + return undefined; + } + return response.body.hits.hits[0]._id; + } catch (error) { + const errorMessage = JSON.stringify(error.meta?.body) || error; + throw new Error(`search ${name} agent failed, reason: ` + errorMessage); + } +}; diff --git a/server/routes/summary_routes.ts b/server/routes/summary_routes.ts new file mode 100644 index 00000000..dcdc6879 --- /dev/null +++ b/server/routes/summary_routes.ts @@ -0,0 +1,127 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { schema } from '@osd/config-schema'; +import { IRouter } from '../../../../src/core/server'; +import { SUMMARY_ASSISTANT_API } from '../../common/constants/llm'; +import { getOpenSearchClientTransport } from '../utils/get_opensearch_client_transport'; +import { getAgent, searchAgentByName } from './get_agent'; +import { AssistantServiceSetup } from '../services/assistant_service'; + +const SUMMARY_AGENT_CONFIG_ID = 'os_summary'; +const OS_INSIGHT_AGENT_CONFIG_ID = 'os_insight'; +let osInsightAgentId: string | undefined; +let userInsightAgentId: string | undefined; + +export function registerSummaryAssistantRoutes( + router: IRouter, + assistantService: AssistantServiceSetup +) { + router.post( + { + path: SUMMARY_ASSISTANT_API.SUMMARIZE, + validate: { + body: schema.object({ + type: schema.string(), + insightType: schema.maybe(schema.string()), + question: schema.string(), + context: schema.maybe(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 assistantClient = assistantService.getScopedClient(req, context); + const response = await assistantClient.executeAgentByName(SUMMARY_AGENT_CONFIG_ID, { + context: req.body.context, + question: req.body.question, + }); + let summary; + let insightAgentIdExists = false; + try { + if (req.body.insightType) { + // We have separate agent for os_insight and user_insight. And for user_insight, we can + // only get it by searching on name since it is not stored in agent config. + if (req.body.insightType === 'os_insight') { + if (!osInsightAgentId) { + osInsightAgentId = await getAgent(OS_INSIGHT_AGENT_CONFIG_ID, client); + } + insightAgentIdExists = !!osInsightAgentId; + } else if (req.body.insightType === 'user_insight') { + if (req.body.type === 'alerts') { + if (!userInsightAgentId) { + userInsightAgentId = await searchAgentByName('KB_For_Alert_Insight', client); + } + } + insightAgentIdExists = !!userInsightAgentId; + } + } + } catch (e) { + context.assistant_plugin.logger.info( + `Cannot find insight agent for ${req.body.insightType}` + ); + } + try { + summary = response.body.inference_results[0].output[0].result; + return res.ok({ body: { summary, insightAgentIdExists } }); + } catch (e) { + return res.internalError(); + } + }) + ); + router.post( + { + path: SUMMARY_ASSISTANT_API.INSIGHT, + validate: { + body: schema.object({ + summaryType: schema.string(), + insightType: schema.string(), + summary: schema.string(), + context: 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 insightAgentId = + req.body.insightType === 'os_insight' ? osInsightAgentId : userInsightAgentId; + if (!insightAgentId) { + context.assistant_plugin.logger.info( + `Cannot find insight agent for ${req.body.insightType}` + ); + return res.internalError(); + } + const assistantClient = assistantService.getScopedClient(req, context); + const response = await assistantClient.executeAgent(insightAgentId, { + context: req.body.context, + summary: req.body.summary, + question: req.body.question, + }); + try { + let result = response.body.inference_results[0].output[0].result; + result = JSON.parse(result).output.text; + return res.ok({ body: result }); + } catch (e) { + return res.internalError(); + } finally { + // Reset userInsightAgentId in case users update their insight agent. + userInsightAgentId = undefined; + } + }) + ); +}