Skip to content

Commit

Permalink
Support insight with RAG (#266)
Browse files Browse the repository at this point in the history
* Support insight with RAG

Signed-off-by: Heng Qian <[email protected]>

* Add change in CHANGELOG.md

Signed-off-by: Heng Qian <[email protected]>

* Put footer in the same panel with content to match UX design

Signed-off-by: Heng Qian <[email protected]>

* Refine alert prompt

Signed-off-by: Heng Qian <[email protected]>

* set CSS scrollbar-width to thin

Signed-off-by: Heng Qian <[email protected]>

* Hide insight agent id from front-end

Signed-off-by: Heng Qian <[email protected]>

* Change summary agent config id

Signed-off-by: Heng Qian <[email protected]>

* Address comments

Signed-off-by: Heng Qian <[email protected]>

* Fix UT

Signed-off-by: Heng Qian <[email protected]>

* Change agent execute API

Signed-off-by: Heng Qian <[email protected]>

* Remove prompt from node JS server

Signed-off-by: Heng Qian <[email protected]>

* Replace CSS with component property

Signed-off-by: Heng Qian <[email protected]>

---------

Signed-off-by: Heng Qian <[email protected]>
  • Loading branch information
qianheng-aws committed Sep 11, 2024
1 parent 4d60d51 commit 4e68047
Show file tree
Hide file tree
Showing 12 changed files with 586 additions and 227 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
- 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))
5 changes: 5 additions & 0 deletions common/constants/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/`,
Expand Down
32 changes: 32 additions & 0 deletions public/assets/shiny_sparkle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions public/assets/sparkle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
215 changes: 122 additions & 93 deletions public/components/incontext_insight/generate_popover_body.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -37,163 +37,192 @@ 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(
<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' }],
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(
<GeneratePopoverBody
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

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

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

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

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(
<GeneratePopoverBody
aria-label="test-generated-popover"
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

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(
<GeneratePopoverBody
aria-label="test-generated-popover"
incontextInsight={incontextInsightMock}
httpSetup={mockHttpSetup}
closePopover={closePopoverMock}
/>
);

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();
});
});
Loading

0 comments on commit 4e68047

Please sign in to comment.