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

Add autotitling of conversations #41

Merged
merged 6 commits into from
Aug 14, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import React, { useState } from 'react';
import { ObservabilityAIAssistantChatServiceProvider } from '../../context/observability_ai_assistant_chat_service_provider';
import { useAbortableAsync } from '../../hooks/use_abortable_async';
import { useConversation } from '../../hooks/use_conversation';
import { useGenAIConnectors } from '../../hooks/use_genai_connectors';
import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant';
import { EMPTY_CONVERSATION_TITLE } from '../../i18n';
import { AssistantAvatar } from '../assistant_avatar';
import { ChatFlyout } from '../chat/chat_flyout';

export function ObservabilityAIAssistantActionMenuItem() {
const service = useObservabilityAIAssistant();
const connectors = useGenAIConnectors();

const [isOpen, setIsOpen] = useState(false);

Expand All @@ -34,6 +36,7 @@ export function ObservabilityAIAssistantActionMenuItem() {

const { conversation, displayedMessages, setDisplayedMessages, save } = useConversation({
conversationId,
connectorId: connectors.selectedConnector,
});

if (!service.isEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,20 @@ const defaultProps: ComponentProps<typeof Component> = {
trigger: MessageRole.Assistant,
},
actions: {
canEdit: true,
canEdit: false,
canCopy: true,
canGiveFeedback: true,
canRegenerate: true,
},
}),
buildFunctionChatItem({
content: '{ "message": "The arguments are wrong" }',
error: new Error(),
actions: {
canRegenerate: false,
canEdit: true,
canGiveFeedback: false,
canCopy: true,
},
}),
buildAssistantChatItem({
Expand All @@ -98,6 +104,9 @@ const defaultProps: ComponentProps<typeof Component> = {
},
actions: {
canEdit: true,
canCopy: true,
canGiveFeedback: true,
canRegenerate: true,
},
}),
buildFunctionChatItem({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const titleClassName = css`
text-transform: uppercase;
`;

const newChatButtonWrapperClassName = css`
padding-bottom: 5px;
`;

export function ConversationList({
selected,
onClickNewChat,
Expand Down Expand Up @@ -101,6 +105,7 @@ export function ConversationList({
isActive={conversation.id === selected}
isDisabled={loading}
href={conversation.href}
wrapText
extraAction={
conversation.id
? {
Expand Down Expand Up @@ -135,7 +140,7 @@ export function ConversationList({
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="s" hasBorder={false} hasShadow={false}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow>
<EuiFlexItem grow className={newChatButtonWrapperClassName}>
<NewChatButton onClick={onClickNewChat} />
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
* 2.0.
*/

import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
EuiButtonEmpty,
EuiContextMenu,
EuiContextMenuPanel,
EuiHighlight,
EuiPopover,
EuiSelectable,
EuiSelectableOption,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import type { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components/selectable/selectable_option';
import { i18n } from '@kbn/i18n';
import { FunctionDefinition } from '../../../common/types';
import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service';

interface FunctionListOption {
label: string;
searchableLabel: string;
}

export function FunctionListPopover({
selectedFunctionName,
onSelectFunction,
Expand All @@ -27,17 +33,78 @@ export function FunctionListPopover({
onSelectFunction: (func: string) => void;
disabled: boolean;
}) {
const chatService = useObservabilityAIAssistantChatService();
const { getFunctions } = useObservabilityAIAssistantChatService();
const filterRef = useRef<HTMLInputElement | null>(null);

const [functionOptions, setFunctionOptions] = useState<
Array<EuiSelectableOption<FunctionListOption>>
>([]);

const [isFunctionListOpen, setIsFunctionListOpen] = useState(false);

const handleClickFunctionList = () => {
setIsFunctionListOpen(!isFunctionListOpen);
};

const handleSelectFunction = (func: FunctionDefinition) => {
const handleSelectFunction = (func: EuiSelectableOption<FunctionListOption>) => {
setIsFunctionListOpen(false);
onSelectFunction(func.options.name);
onSelectFunction(func.label);
};

useEffect(() => {
const keyboardListener = (event: KeyboardEvent) => {
if (event.shiftKey && event.code === 'Digit4') {
setIsFunctionListOpen(true);
}
};

window.addEventListener('keyup', keyboardListener);

return () => {
window.removeEventListener('keyup', keyboardListener);
};
}, []);

useEffect(() => {
if (isFunctionListOpen && filterRef.current) {
filterRef.current.focus();
}
}, [isFunctionListOpen]);

useEffect(() => {
const options = getFunctions().map((func) => ({
label: func.options.name,
searchableLabel: func.options.descriptionForUser,
checked:
func.options.name === selectedFunctionName
? ('on' as EuiSelectableOptionCheckedType)
: ('off' as EuiSelectableOptionCheckedType),
}));

setFunctionOptions(options);
}, [getFunctions, selectedFunctionName]);

const renderCountryOption = (
option: EuiSelectableOption<FunctionListOption>,
searchValue: string
) => {
return (
<>
<EuiText size="s">
<p>
<strong>
<EuiHighlight search={searchValue}>{option.label}</EuiHighlight>{' '}
</strong>
</p>
</EuiText>
<EuiSpacer size="xs" />
<EuiText size="s">
<p style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>
<EuiHighlight search={searchValue}>{option.searchableLabel || ''}</EuiHighlight>
</p>
</EuiText>
</>
);
};

return (
Expand All @@ -53,43 +120,51 @@ export function FunctionListPopover({
>
{selectedFunctionName
? selectedFunctionName
: i18n.translate('xpack.observabilityAiAssistant.prompt.callFunction', {
: i18n.translate('xpack.observabilityAiAssistant.prompt.functionList.callFunction', {
defaultMessage: 'Call function',
})}
</EuiButtonEmpty>
}
closePopover={handleClickFunctionList}
css={{ maxWidth: 400 }}
panelPaddingSize="none"
isOpen={isFunctionListOpen}
>
<EuiContextMenuPanel size="s">
<EuiContextMenu
initialPanelId={0}
panels={[
{
id: 0,
width: 500,
items: chatService.getFunctions().map((func) => ({
name: (
<>
<EuiText size="s">
<p>
<strong>{func.options.name}</strong>
</p>
</EuiText>
<EuiSpacer size="xs" />
<EuiText size="s">
<p>{func.options.descriptionForUser}</p>
</EuiText>
</>
),
onClick: () => handleSelectFunction(func),
})),
},
]}
/>
</EuiContextMenuPanel>
<EuiSelectable
aria-label={i18n.translate(
'xpack.observabilityAiAssistant.prompt.functionList.functionList',
{
defaultMessage: 'Function list',
}
)}
listProps={{
isVirtualized: false,
showIcons: false,
}}
options={functionOptions}
renderOption={renderCountryOption}
searchable
searchProps={{
'data-test-subj': 'searchFiltersList',
inputRef: (node) => (filterRef.current = node),
placeholder: i18n.translate('xpack.observabilityAiAssistant.prompt.functionList.filter', {
defaultMessage: 'Filter',
}),
}}
singleSelection
onChange={(functions) => {
const selectedFunction = functions.filter((fn) => fn.checked !== 'off');
if (selectedFunction && selectedFunction.length === 1) {
handleSelectFunction({ ...selectedFunction[0], checked: 'on' });
}
}}
>
{(list, search) => (
<div style={{ overflow: 'hidden' }}>
{search}
<div style={{ width: 500, height: 350, overflowY: 'scroll' }}>{list}</div>
</div>
)}
</EuiSelectable>
</EuiPopover>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { merge, omit } from 'lodash';
import { Dispatch, SetStateAction, useState } from 'react';
import type { Conversation, Message } from '../../common';
import { type Conversation, type Message } from '../../common';
import type { ConversationCreateRequest } from '../../common/types';
import { ObservabilityAIAssistantChatService } from '../types';
import { useAbortableAsync, type AbortableAsyncState } from './use_abortable_async';
Expand All @@ -18,14 +18,16 @@ import { createNewConversation } from './use_timeline';
export function useConversation({
conversationId,
chatService,
connectorId,
}: {
conversationId?: string;
chatService?: ObservabilityAIAssistantChatService;
connectorId: string | undefined;
}): {
conversation: AbortableAsyncState<ConversationCreateRequest | Conversation | undefined>;
displayedMessages: Message[];
setDisplayedMessages: Dispatch<SetStateAction<Message[]>>;
save: (messages: Message[]) => Promise<Conversation>;
save: (messages: Message[], handleRefreshConversations?: () => void) => Promise<Conversation>;
} {
const service = useObservabilityAIAssistant();

Expand Down Expand Up @@ -67,11 +69,12 @@ export function useConversation({
conversation,
displayedMessages,
setDisplayedMessages,
save: (messages: Message[]) => {
save: (messages: Message[], handleRefreshConversations?: () => void) => {
const conversationObject = conversation.value!;

return conversationId
? service
.callApi(`POST /internal/observability_ai_assistant/conversation/{conversationId}`, {
.callApi(`PUT /internal/observability_ai_assistant/conversation/{conversationId}`, {
signal: null,
params: {
path: {
Expand Down Expand Up @@ -100,14 +103,38 @@ export function useConversation({
throw err;
})
: service
.callApi(`PUT /internal/observability_ai_assistant/conversation`, {
.callApi(`POST /internal/observability_ai_assistant/conversation`, {
signal: null,
params: {
body: {
conversation: merge({}, conversationObject, { messages }),
},
},
})
.then((nextConversation) => {
if (connectorId) {
service
.callApi(
`PUT /internal/observability_ai_assistant/conversation/{conversationId}/auto_title`,
{
signal: null,
params: {
path: {
conversationId: nextConversation.conversation.id,
},
body: {
connectorId,
},
},
}
)
.then(() => {
handleRefreshConversations?.();
return conversation.refresh();
});
}
return nextConversation;
})
.catch((err) => {
notifications.toasts.addError(err, {
title: i18n.translate('xpack.observabilityAiAssistant.errorCreatingConversation', {
Expand Down
Loading
Loading