Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support insight with RAG #266

Merged
merged 16 commits into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading