Skip to content

Commit

Permalink
[Incontext Insights] wrapper component and service (#53) (#144)
Browse files Browse the repository at this point in the history
* [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]>
(cherry picked from commit 5e4c971)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 2851b8a commit 6dce0f6
Show file tree
Hide file tree
Showing 25 changed files with 1,108 additions and 50 deletions.
19 changes: 19 additions & 0 deletions common/types/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema, TypeOf } from '@osd/config-schema';

export const configSchema = schema.object({
// TODO: add here to prevent this plugin from being loaded
// enabled: schema.boolean({ defaultValue: true }),
chat: schema.object({
enabled: schema.boolean({ defaultValue: false }),
}),
incontextInsight: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
});

export type ConfigSchema = TypeOf<typeof configSchema>;
62 changes: 62 additions & 0 deletions docs/incontext_insight/component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# IncontextInsight

`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.


## Props

`IncontextInsight` takes the following props:

- `children`: ReactNode. The child components to be rendered within the `IncontextInsight` context.

## Usage

```typescriptreact
import { IncontextInsight } from '../incontext_insight';
<IncontextInsight>
<div>Your content here</div>
</IncontextInsight>
```

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.
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
if the Assistant Dashboards plugin is available then on plugin setup call renderIncontextInsightComponent passing the same props. For example:

```typescriptreact
import React from 'react';
import { OuiLink } from '@opensearch-project/oui';
// export default component
export let ExampleIncontextInsightComponent = (props: any) => <div {...props} />;
//====== plugin setup ======//
// check Assistant Dashboards is installed
if (assistantDashboards) {
// update default component
ExampleIncontextInsightComponent = (props: any) => (
<>{assistantDashboards.renderIncontextInsight(props)}</>
);
}
//====== plugin setup ======//
function ExampleComponent() {
return (
// Use your component
<ExampleIncontextInsightComponent>
<OuiLink
key="exampleKey"
data-test-subj="exampleSubject"
href="http://example.com"
>
Example Link
</OuiLink>
</ExampleIncontextInsightComponent>
);
}
export default ExampleComponent;
```

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.
62 changes: 62 additions & 0 deletions docs/incontext_insight/registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# IncontextInsightRegistry

`IncontextInsightRegistry` is a TypeScript class that manages the registration and retrieval of `IncontextInsight` items.

## Methods

### open(item: IncontextInsight, suggestion: string)

This method emits an 'onSuggestion' event with the provided suggestion.

### register(item: IncontextInsight | IncontextInsight[])

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.

### get(key: string): IncontextInsight

This method retrieves an `IncontextInsight` item from the registry using its key.

### getAll(): IncontextInsight[]

This method retrieves all `IncontextInsight` items from the registry.

### getSummary(key: string)

This method retrieves the summary of an `IncontextInsight` item using its key.

## Usage

```typescript
import { IncontextInsightRegistry } from './incontext_insight_registry';

const registry = new IncontextInsightRegistry();

// Register a single item
registry.register({
key: 'item1',
summary: 'This is item 1',
suggestions: ['suggestion1', 'suggestion2'],
});

// Register multiple items
registry.register([
{
key: 'item2',
summary: 'This is item 2',
suggestions: ['suggestion3', 'suggestion4'],
},
{
key: 'item3',
summary: 'This is item 3',
suggestions: ['suggestion5', 'suggestion6'],
},
]);

// Retrieve an item
const item1 = registry.get('item1');

// Retrieve all items
const allItems = registry.getAll();

// Retrieve an item's summary
const item1Summary = registry.getSummary('item1');
81 changes: 81 additions & 0 deletions docs/incontext_insight/service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# IncontextInsights and Chat Interaction

`IncontextInsights` can be used to enhance the chat experience by providing contextual insights based on the ongoing conversation.

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.

Here's an example of how you might use the `assistantDashboards` plugin in the `AlertingPlugin` setup:

```typescript
import { CoreSetup } from 'src/core/public';
import { AssistantPublicPluginSetup } from 'src/plugins/assistant/public';

interface AlertingSetupDeps {
expressions: any;
uiActions: any;
assistantDashboards?: AssistantPublicPluginSetup;
}

class AlertingPlugin implements Plugin<{}, {}, AlertingSetupDeps> {
public setup(core: CoreSetup, { assistantDashboards }: AlertingSetupDeps) {
if (assistantDashboards) {
// Use the assistantDashboards plugin
assistantDashboards.registerIncontextInsight([
{
key: 'query_level_monitor',
summary:
'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.',
suggestions: ['How to better configure my monitor?'],
},
{
key: 'content_panel_Data source',
summary:
'OpenSearch data sources are the applications that OpenSearch can connect to and ingest data from.',
suggestions: ['What are the indices in my cluster?'],
},
]);
}
}
}
```

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.

## How it works

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.

## Usage

Here's an example of how you might use `IncontextInsights` in a chat application:

```typescript
import { IncontextInsightRegistry } from './incontext_insight_registry';

const registry = new IncontextInsightRegistry();

// Register some insights
registry.register([
{ key: 'greeting', summary: 'This is a greeting', suggestions: ['Hello', 'Hi', 'Hey'] },
{ key: 'farewell', summary: 'This is a farewell', suggestions: ['Goodbye', 'See you', 'Take care'] },
]);

// When a message is sent or received...
const message = 'Hello, how are you?';

// Query the registry for relevant insights
const insights = registry.getAll().filter(insight => message.includes(insight.summary));

// Display the insights in the chat interface
insights.forEach(insight => {
console.log(`Suggestion: ${insight.suggestions[0]}`);
});
```

## Disabling incontext insights

By default, `IncontextInsights` will be enabled if chat is enabled. The following configuration disables this component:

```yaml
assistant.incontextInsight.enabled: false
```
11 changes: 11 additions & 0 deletions public/chat_header_button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { BehaviorSubject } from 'rxjs';

let mockSend: jest.Mock;
let mockLoadChat: jest.Mock;
let mockIncontextInsightRegistry: jest.Mock;

jest.mock('./hooks/use_chat_actions', () => {
mockSend = jest.fn();
Expand Down Expand Up @@ -46,6 +47,16 @@ jest.mock('./chat_flyout', () => {
};
});

jest.mock('./services', () => {
mockIncontextInsightRegistry = jest.fn().mockReturnValue({
on: jest.fn(),
off: jest.fn(),
});
return {
getIncontextInsightRegistry: mockIncontextInsightRegistry,
};
});

describe('<HeaderChatButton />', () => {
afterEach(() => {
jest.clearAllMocks();
Expand Down
25 changes: 25 additions & 0 deletions public/chat_header_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import classNames from 'classnames';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useEffectOnce } from 'react-use';
import { ApplicationStart } from '../../../src/core/public';
// TODO: Replace with getChrome().logos.Chat.url
import chatIcon from './assets/chat.svg';
import { getIncontextInsightRegistry } from './services';
import { ChatFlyout } from './chat_flyout';
import { ChatContext, IChatContext } from './contexts/chat_context';
import { SetContext } from './contexts/set_context';
Expand Down Expand Up @@ -41,6 +43,7 @@ export const HeaderChatButton = (props: HeaderChatButtonProps) => {
const [inputFocus, setInputFocus] = useState(false);
const flyoutFullScreen = chatSize === 'fullscreen';
const inputRef = useRef<HTMLInputElement>(null);
const registry = getIncontextInsightRegistry();

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

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

useEffect(() => {
const handleSuggestion = (event: { suggestion: string }) => {
if (!flyoutVisible) {
// open chat window
setFlyoutVisible(true);
// start a new chat
props.assistantActions.loadChat();
}
// send message
props.assistantActions.send({
type: 'input',
contentType: 'text',
content: event.suggestion,
context: { appId },
});
};
registry.on('onSuggestion', handleSuggestion);
return () => {
registry.off('onSuggestion', handleSuggestion);
};
}, [appId, flyoutVisible, props.assistantActions, registry]);

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

import React from 'react';
import { render, cleanup } from '@testing-library/react';
import { IncontextInsight } from '../incontext_insight';
import { getChrome, getNotifications, getIncontextInsightRegistry } from '../../services';

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

beforeEach(() => {
(getChrome as jest.Mock).mockImplementation(() => ({
logos: 'mocked logos',
}));
(getNotifications as jest.Mock).mockImplementation(() => ({
toasts: {
addSuccess: jest.fn(),
addError: jest.fn(),
},
}));
(getIncontextInsightRegistry as jest.Mock).mockImplementation(() => {});
});

describe('IncontextInsight', () => {
afterEach(cleanup);

it('renders the child', () => {
const { getByText } = render(
<IncontextInsight>
<div>Test child</div>
</IncontextInsight>
);

expect(getByText('Test child')).toBeInTheDocument();
});

it('renders the children', () => {
const { getByText } = render(
<IncontextInsight>
<div>
<h3>Test child</h3>
<div>Test child 2</div>
</div>
</IncontextInsight>
);

expect(getByText('Test child')).toBeInTheDocument();
});
});
Loading

0 comments on commit 6dce0f6

Please sign in to comment.