Skip to content

Commit 5e4c971

Browse files
authored
[Incontext Insights] wrapper component and service (#53)
* [Palantir] wrapper component and service Registry and component to be used by plugins. Example of usage: opensearch-project/alerting-dashboards-plugin#852 Signed-off-by: Kawika Avilla <[email protected]> * recursion Signed-off-by: Kawika Avilla <[email protected]> * more styling on component Signed-off-by: Kawika Avilla <[email protected]> * one more try Signed-off-by: Kawika Avilla <[email protected]> * Using wrapper component Signed-off-by: Kawika Avilla <[email protected]> * almost Signed-off-by: Kawika Avilla <[email protected]> * add arrow Signed-off-by: Kawika Avilla <[email protected]> * cross Signed-off-by: Kawika Avilla <[email protected]> * renamed Signed-off-by: Kawika Avilla <[email protected]> * no destory Signed-off-by: Kawika Avilla <[email protected]> * hacky styling Signed-off-by: Kawika Avilla <[email protected]> * some rough tests and readme Signed-off-by: Kawika Avilla <[email protected]> * add more doc Signed-off-by: Kawika Avilla <[email protected]> * re-add type Signed-off-by: Kawika Avilla <[email protected]> * fix tests Signed-off-by: Kawika Avilla <[email protected]> * add i18n Signed-off-by: Kawika Avilla <[email protected]> * enable target feature Signed-off-by: Kawika Avilla <[email protected]> * remove invalid test now Signed-off-by: Kawika Avilla <[email protected]> * missing palantir ref Signed-off-by: Kawika Avilla <[email protected]> * make callable render function Signed-off-by: Kawika Avilla <[email protected]> * update docs Signed-off-by: Kawika Avilla <[email protected]> * cleaner incode styling Signed-off-by: Kawika Avilla <[email protected]> * build config Signed-off-by: Kawika Avilla <[email protected]> * move to server Signed-off-by: Kawika Avilla <[email protected]> * not available for assets Signed-off-by: Kawika Avilla <[email protected]> * fix path Signed-off-by: Kawika Avilla <[email protected]> * check on call Signed-off-by: Kawika Avilla <[email protected]> * dont give id Signed-off-by: Kawika Avilla <[email protected]> * clean up styles one more time Signed-off-by: Kawika Avilla <[email protected]> * add config to disable incontext not matter what Signed-off-by: Kawika Avilla <[email protected]> * add doc Signed-off-by: Kawika Avilla <[email protected]> * update changelog Signed-off-by: Kawika Avilla <[email protected]> * more tests Signed-off-by: Kawika Avilla <[email protected]> * address naming of classes Signed-off-by: Kawika Avilla <[email protected]> * Add unused container Signed-off-by: Kawika Avilla <[email protected]> * Removed default enabled config Signed-off-by: Kawika Avilla <[email protected]> --------- Signed-off-by: Kawika Avilla <[email protected]>
1 parent 3f737cf commit 5e4c971

26 files changed

+1109
-50
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1010
- Refactor the code to get root agent id by calling the API in ml-commons plugin ([#128](https://github.com/opensearch-project/dashboards-assistant/pull/128))
1111
- Set verbose to false ([#131](https://github.com/opensearch-project/dashboards-assistant/pull/131))
1212
- Fix: comply with the field change of agent framework ([#137](https://github.com/opensearch-project/dashboards-assistant/pull/137))
13+
- Add incontext insight component ([#53](https://github.com/opensearch-project/dashboards-assistant/pull/53))

Diff for: common/types/config.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { schema, TypeOf } from '@osd/config-schema';
7+
8+
export const configSchema = schema.object({
9+
// TODO: add here to prevent this plugin from being loaded
10+
// enabled: schema.boolean({ defaultValue: true }),
11+
chat: schema.object({
12+
enabled: schema.boolean({ defaultValue: false }),
13+
}),
14+
incontextInsight: schema.object({
15+
enabled: schema.boolean({ defaultValue: true }),
16+
}),
17+
});
18+
19+
export type ConfigSchema = TypeOf<typeof configSchema>;

Diff for: docs/incontext_insight/component.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# IncontextInsight
2+
3+
`IncontextInsight` is a React component that provides a context for displaying insights in your application. It uses services such as `getChrome`, `getNotifications`, and `getIncontextInsightRegistry` to manage and display insights.
4+
5+
6+
## Props
7+
8+
`IncontextInsight` takes the following props:
9+
10+
- `children`: ReactNode. The child components to be rendered within the `IncontextInsight` context.
11+
12+
## Usage
13+
14+
```typescriptreact
15+
import { IncontextInsight } from '../incontext_insight';
16+
17+
<IncontextInsight>
18+
<div>Your content here</div>
19+
</IncontextInsight>
20+
```
21+
22+
In usage of a plugin, IncontextInsight is used to wrap an element. The div and its content will be rendered within the context provided by IncontextInsight.
23+
To ensure your plugin does not require the Assistant Dashboards plugin bundle define a functional component that will render a div with props by default and
24+
if the Assistant Dashboards plugin is available then on plugin setup call renderIncontextInsightComponent passing the same props. For example:
25+
26+
```typescriptreact
27+
import React from 'react';
28+
import { OuiLink } from '@opensearch-project/oui';
29+
30+
// export default component
31+
export let ExampleIncontextInsightComponent = (props: any) => <div {...props} />;
32+
33+
//====== plugin setup ======//
34+
// check Assistant Dashboards is installed
35+
if (assistantDashboards) {
36+
// update default component
37+
ExampleIncontextInsightComponent = (props: any) => (
38+
<>{assistantDashboards.renderIncontextInsight(props)}</>
39+
);
40+
}
41+
//====== plugin setup ======//
42+
43+
44+
function ExampleComponent() {
45+
return (
46+
// Use your component
47+
<ExampleIncontextInsightComponent>
48+
<OuiLink
49+
key="exampleKey"
50+
data-test-subj="exampleSubject"
51+
href="http://example.com"
52+
>
53+
Example Link
54+
</OuiLink>
55+
</ExampleIncontextInsightComponent>
56+
);
57+
}
58+
59+
export default ExampleComponent;
60+
```
61+
62+
The ExampleIncontextInsightComponent is a React component used in this code to wrap an OuiLink component with a `<div>` or the functional component defined by Assistant Dashboards. The OuiLink component is a part of the OpenSearch UI framework and is used to create a hyperlink.

Diff for: docs/incontext_insight/registry.md

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# IncontextInsightRegistry
2+
3+
`IncontextInsightRegistry` is a TypeScript class that manages the registration and retrieval of `IncontextInsight` items.
4+
5+
## Methods
6+
7+
### open(item: IncontextInsight, suggestion: string)
8+
9+
This method emits an 'onSuggestion' event with the provided suggestion.
10+
11+
### register(item: IncontextInsight | IncontextInsight[])
12+
13+
This method registers a single `IncontextInsight` item or an array of `IncontextInsight` items. Each item is mapped using the `mapper` method before being stored in the registry.
14+
15+
### get(key: string): IncontextInsight
16+
17+
This method retrieves an `IncontextInsight` item from the registry using its key.
18+
19+
### getAll(): IncontextInsight[]
20+
21+
This method retrieves all `IncontextInsight` items from the registry.
22+
23+
### getSummary(key: string)
24+
25+
This method retrieves the summary of an `IncontextInsight` item using its key.
26+
27+
## Usage
28+
29+
```typescript
30+
import { IncontextInsightRegistry } from './incontext_insight_registry';
31+
32+
const registry = new IncontextInsightRegistry();
33+
34+
// Register a single item
35+
registry.register({
36+
key: 'item1',
37+
summary: 'This is item 1',
38+
suggestions: ['suggestion1', 'suggestion2'],
39+
});
40+
41+
// Register multiple items
42+
registry.register([
43+
{
44+
key: 'item2',
45+
summary: 'This is item 2',
46+
suggestions: ['suggestion3', 'suggestion4'],
47+
},
48+
{
49+
key: 'item3',
50+
summary: 'This is item 3',
51+
suggestions: ['suggestion5', 'suggestion6'],
52+
},
53+
]);
54+
55+
// Retrieve an item
56+
const item1 = registry.get('item1');
57+
58+
// Retrieve all items
59+
const allItems = registry.getAll();
60+
61+
// Retrieve an item's summary
62+
const item1Summary = registry.getSummary('item1');

Diff for: docs/incontext_insight/service.md

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# IncontextInsights and Chat Interaction
2+
3+
`IncontextInsights` can be used to enhance the chat experience by providing contextual insights based on the ongoing conversation.
4+
5+
The `assistantDashboards` is should be an optional property in the `PluginSetupDeps` interface. It represents a plugin that might be available during the setup phase of a plugin.
6+
7+
Here's an example of how you might use the `assistantDashboards` plugin in the `AlertingPlugin` setup:
8+
9+
```typescript
10+
import { CoreSetup } from 'src/core/public';
11+
import { AssistantPublicPluginSetup } from 'src/plugins/assistant/public';
12+
13+
interface AlertingSetupDeps {
14+
expressions: any;
15+
uiActions: any;
16+
assistantDashboards?: AssistantPublicPluginSetup;
17+
}
18+
19+
class AlertingPlugin implements Plugin<{}, {}, AlertingSetupDeps> {
20+
public setup(core: CoreSetup, { assistantDashboards }: AlertingSetupDeps) {
21+
if (assistantDashboards) {
22+
// Use the assistantDashboards plugin
23+
assistantDashboards.registerIncontextInsight([
24+
{
25+
key: 'query_level_monitor',
26+
summary:
27+
'Per query monitors are a type of alert monitor that can be used to identify and alert on specific queries that are run against an OpenSearch index; for example, queries that detect and respond to anomalies in specific queries. Per query monitors only trigger one alert at a time.',
28+
suggestions: ['How to better configure my monitor?'],
29+
},
30+
{
31+
key: 'content_panel_Data source',
32+
summary:
33+
'OpenSearch data sources are the applications that OpenSearch can connect to and ingest data from.',
34+
suggestions: ['What are the indices in my cluster?'],
35+
},
36+
]);
37+
}
38+
}
39+
}
40+
```
41+
42+
In this example, we're checking if the `assistantDashboards` plugin is available during the setup phase. If it is, we're using it to register incontext insights for specific keys with a seed suggestion.
43+
44+
## How it works
45+
46+
When a chat message is sent or received, the `IncontextInsightRegistry` can be queried for relevant insights based on the content of the message. These insights can then be displayed in the chat interface to provide additional information or suggestions to the user.
47+
48+
## Usage
49+
50+
Here's an example of how you might use `IncontextInsights` in a chat application:
51+
52+
```typescript
53+
import { IncontextInsightRegistry } from './incontext_insight_registry';
54+
55+
const registry = new IncontextInsightRegistry();
56+
57+
// Register some insights
58+
registry.register([
59+
{ key: 'greeting', summary: 'This is a greeting', suggestions: ['Hello', 'Hi', 'Hey'] },
60+
{ key: 'farewell', summary: 'This is a farewell', suggestions: ['Goodbye', 'See you', 'Take care'] },
61+
]);
62+
63+
// When a message is sent or received...
64+
const message = 'Hello, how are you?';
65+
66+
// Query the registry for relevant insights
67+
const insights = registry.getAll().filter(insight => message.includes(insight.summary));
68+
69+
// Display the insights in the chat interface
70+
insights.forEach(insight => {
71+
console.log(`Suggestion: ${insight.suggestions[0]}`);
72+
});
73+
```
74+
75+
## Disabling incontext insights
76+
77+
By default, `IncontextInsights` will be enabled if chat is enabled. The following configuration disables this component:
78+
79+
```yaml
80+
assistant.incontextInsight.enabled: false
81+
```

Diff for: public/chat_header_button.test.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { BehaviorSubject } from 'rxjs';
1313

1414
let mockSend: jest.Mock;
1515
let mockLoadChat: jest.Mock;
16+
let mockIncontextInsightRegistry: jest.Mock;
1617

1718
jest.mock('./hooks/use_chat_actions', () => {
1819
mockSend = jest.fn();
@@ -46,6 +47,16 @@ jest.mock('./chat_flyout', () => {
4647
};
4748
});
4849

50+
jest.mock('./services', () => {
51+
mockIncontextInsightRegistry = jest.fn().mockReturnValue({
52+
on: jest.fn(),
53+
off: jest.fn(),
54+
});
55+
return {
56+
getIncontextInsightRegistry: mockIncontextInsightRegistry,
57+
};
58+
});
59+
4960
describe('<HeaderChatButton />', () => {
5061
afterEach(() => {
5162
jest.clearAllMocks();

Diff for: public/chat_header_button.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import classNames from 'classnames';
88
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
99
import { useEffectOnce } from 'react-use';
1010
import { ApplicationStart } from '../../../src/core/public';
11+
// TODO: Replace with getChrome().logos.Chat.url
1112
import chatIcon from './assets/chat.svg';
13+
import { getIncontextInsightRegistry } from './services';
1214
import { ChatFlyout } from './chat_flyout';
1315
import { ChatContext, IChatContext } from './contexts/chat_context';
1416
import { SetContext } from './contexts/set_context';
@@ -41,6 +43,7 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
4143
const [inputFocus, setInputFocus] = useState(false);
4244
const flyoutFullScreen = chatSize === 'fullscreen';
4345
const inputRef = useRef<HTMLInputElement>(null);
46+
const registry = getIncontextInsightRegistry();
4447

4548
if (!flyoutLoaded && flyoutVisible) flyoutLoaded = true;
4649

@@ -138,6 +141,28 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
138141
};
139142
}, [props.userHasAccess]);
140143

144+
useEffect(() => {
145+
const handleSuggestion = (event: { suggestion: string }) => {
146+
if (!flyoutVisible) {
147+
// open chat window
148+
setFlyoutVisible(true);
149+
// start a new chat
150+
props.assistantActions.loadChat();
151+
}
152+
// send message
153+
props.assistantActions.send({
154+
type: 'input',
155+
contentType: 'text',
156+
content: event.suggestion,
157+
context: { appId },
158+
});
159+
};
160+
registry.on('onSuggestion', handleSuggestion);
161+
return () => {
162+
registry.off('onSuggestion', handleSuggestion);
163+
};
164+
}, [appId, flyoutVisible, props.assistantActions, registry]);
165+
141166
return (
142167
<>
143168
<div className={classNames('llm-chat-header-icon-wrapper')}>
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { render, cleanup } from '@testing-library/react';
8+
import { IncontextInsight } from '../incontext_insight';
9+
import { getChrome, getNotifications, getIncontextInsightRegistry } from '../../services';
10+
11+
jest.mock('../../services');
12+
13+
beforeEach(() => {
14+
(getChrome as jest.Mock).mockImplementation(() => ({
15+
logos: 'mocked logos',
16+
}));
17+
(getNotifications as jest.Mock).mockImplementation(() => ({
18+
toasts: {
19+
addSuccess: jest.fn(),
20+
addError: jest.fn(),
21+
},
22+
}));
23+
(getIncontextInsightRegistry as jest.Mock).mockImplementation(() => {});
24+
});
25+
26+
describe('IncontextInsight', () => {
27+
afterEach(cleanup);
28+
29+
it('renders the child', () => {
30+
const { getByText } = render(
31+
<IncontextInsight>
32+
<div>Test child</div>
33+
</IncontextInsight>
34+
);
35+
36+
expect(getByText('Test child')).toBeInTheDocument();
37+
});
38+
39+
it('renders the children', () => {
40+
const { getByText } = render(
41+
<IncontextInsight>
42+
<div>
43+
<h3>Test child</h3>
44+
<div>Test child 2</div>
45+
</div>
46+
</IncontextInsight>
47+
);
48+
49+
expect(getByText('Test child')).toBeInTheDocument();
50+
});
51+
});

0 commit comments

Comments
 (0)