Skip to content

Commit

Permalink
Initial implementation of context aware alert analysis (#215) (#271)
Browse files Browse the repository at this point in the history
* support context aware alert analysis

Signed-off-by: Hailong Cui <ihailong@amazon.com>

* Render GeneratePopover IncontextInsight component as a button to generate summary

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Remove hardcoded assistant role from the parameter payload

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Make GeneratePopoverBody as independent component

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Update change log

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Add independent GeneratePopoverBody ut and reorgnize constants

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Simplify states of loading to get summary process

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Make IncontextInsight not shareable and each component has its own IncontextInsight

Signed-off-by: Songkan Tang <songkant@amazon.com>

* Enable context aware alert only if feature flag is enabled

Signed-off-by: Songkan Tang <songkant@amazon.com>

---------

Signed-off-by: Hailong Cui <ihailong@amazon.com>
Signed-off-by: Songkan Tang <songkant@amazon.com>
Co-authored-by: Hailong Cui <ihailong@amazon.com>
(cherry picked from commit 32888dd)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md

Signed-off-by: Songkan Tang <songkant@amazon.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
songkant-aws and github-actions[bot] committed Sep 5, 2024
1 parent bf904f9 commit 4df9b30
Show file tree
Hide file tree
Showing 14 changed files with 528 additions and 45 deletions.
3 changes: 3 additions & 0 deletions common/types/chat_saved_object_attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ export interface IInput {
content: string;
context?: {
appId?: string;
content?: string;
datasourceId?: string;
};
messageId?: string;
promptPrefix?: string;
}
export interface IOutput {
type: 'output';
Expand Down
31 changes: 27 additions & 4 deletions public/chat_header_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,23 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
}, []);

useEffect(() => {
const handleSuggestion = (event: { suggestion: string }) => {
const handleSuggestion = (event: {
suggestion: string;
contextContent: string;
datasourceId?: string;
}) => {
if (!flyoutVisible) {
// open chat window
setFlyoutVisible(true);
// start a new chat
props.assistantActions.loadChat();
}
// start a new chat
props.assistantActions.loadChat();
// send message
props.assistantActions.send({
type: 'input',
contentType: 'text',
content: event.suggestion,
context: { appId },
context: { appId, content: event.contextContent, datasourceId: event.datasourceId },
});
};
registry.on('onSuggestion', handleSuggestion);
Expand All @@ -191,6 +195,25 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
};
}, [appId, flyoutVisible, props.assistantActions, registry]);

useEffect(() => {
const handleChatContinuation = (event: {
conversationId?: string;
contextContent: string;
datasourceId?: string;
}) => {
if (!flyoutVisible) {
// open chat window
setFlyoutVisible(true);
}
// continue chat with current conversationId
props.assistantActions.loadChat(event.conversationId);
};
registry.on('onChatContinuation', handleChatContinuation);
return () => {
registry.off('onChatContinuation', handleChatContinuation);
};
}, [appId, flyoutVisible, props.assistantActions, registry]);

return (
<>
<div className={classNames('llm-chat-header-icon-wrapper')}>
Expand Down
199 changes: 199 additions & 0 deletions public/components/incontext_insight/generate_popover_body.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
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';

jest.mock('../../services');

const mockToasts = {
addDanger: jest.fn(),
};

beforeEach(() => {
(getNotifications as jest.Mock).mockImplementation(() => ({
toasts: mockToasts,
}));
(getConfigSchema as jest.Mock).mockReturnValue({
chat: { enabled: true },
});
});

afterEach(cleanup);

const mockPost = jest.fn();
const mockHttpSetup: HttpSetup = ({
post: mockPost,
} as unknown) as HttpSetup; // Mocking HttpSetup

describe('GeneratePopoverBody', () => {
const incontextInsightMock = {
contextProvider: jest.fn(),
suggestions: ['Test summarization question'],
datasourceId: 'test-datasource',
key: 'test-key',
};

const closePopoverMock = jest.fn();

it('renders the generate summary button', () => {
const { getByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

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' }],
});

const { getByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

const button = getByText('Generate summary');
fireEvent.click(button);

// 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));
expect(mockToasts.addDanger).not.toHaveBeenCalled();
});

it('shows loading state while generating summary', async () => {
const { getByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

const button = getByText('Generate summary');
fireEvent.click(button);

// 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(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

const button = getByText('Generate summary');
fireEvent.click(button);

await waitFor(() => {
expect(mockToasts.addDanger).toHaveBeenCalledWith('Generate summary error');
});
});

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' }],
});

const { getByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

const button = getByText('Generate summary');
fireEvent.click(button);

// Wait for the summary to be displayed
await waitFor(() => {
expect(getByText('Generated summary content')).toBeInTheDocument();
});

// Check for continue in chat button
expect(getByText('Continue in chat')).toBeInTheDocument();
});

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' }],
});

const { getByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

const button = getByText('Generate summary');
fireEvent.click(button);

await waitFor(() => {
expect(getByText('Generated summary content')).toBeInTheDocument();
});

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 },
});

const { getByText, queryByText } = render(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

const button = getByText('Generate summary');
fireEvent.click(button);

await waitFor(() => {
expect(getByText('Generated summary content')).toBeInTheDocument();
});

expect(queryByText('Continue in chat')).toBeNull();
expect(mockPost).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 4df9b30

Please sign in to comment.