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

[Observability AI Assistant]: Function registry #19

Merged
merged 2 commits into from
Aug 1, 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@
"js-search": "^1.4.3",
"js-sha256": "^0.9.0",
"js-yaml": "^3.14.1",
"json-schema-to-ts": "^2.9.1",
"json-stable-stringify": "^1.0.1",
"json-stringify-pretty-compact": "1.2.0",
"json-stringify-safe": "5.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Logger } from '@kbn/logging';
import axios, { AxiosInstance, AxiosResponse, AxiosError, AxiosRequestHeaders } from 'axios';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { finished } from 'stream/promises';
import { IncomingMessage } from 'http';
import { assertURL } from './helpers/validators';
import { ActionsConfigurationUtilities } from '../actions_config';
import { SubAction, SubActionRequestParams } from './types';
Expand Down Expand Up @@ -140,6 +142,19 @@ export abstract class SubActionConnector<Config, Secrets> {
`Request to external service failed. Connector Id: ${this.connector.id}. Connector type: ${this.connector.type}. Method: ${error.config.method}. URL: ${error.config.url}`
);

let responseBody = '';

// The error response body may also be a stream, e.g. for the GenAI connector
if (error.response?.config.responseType === 'stream' && error.response?.data) {
const incomingMessage = error.response.data as IncomingMessage;

incomingMessage.on('data', (chunk) => {
responseBody += chunk.toString();
});
await finished(incomingMessage);
error.response.data = JSON.parse(responseBody);
}

const errorMessage = `Status code: ${
error.status ?? error.response?.status
}. Message: ${this.getResponseErrorMessage(error)}`;
Expand Down
57 changes: 54 additions & 3 deletions x-pack/plugins/observability_ai_assistant/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* 2.0.
*/

import { Serializable } from '@kbn/utility-types';
import type { Serializable } from '@kbn/utility-types';
import type { FromSchema } from 'json-schema-to-ts';
import type { JSONSchema } from 'json-schema-to-ts';
import React from 'react';

export enum MessageRole {
System = 'system',
Expand All @@ -24,10 +27,10 @@ export interface Message {
role: MessageRole;
function_call?: {
name: string;
args?: Serializable;
arguments?: string;
trigger: MessageRole.Assistant | MessageRole.User | MessageRole.Elastic;
};
data?: Serializable;
data?: string;
};
}

Expand All @@ -54,3 +57,51 @@ export type ConversationRequestBase = Omit<Conversation, 'user' | 'conversation'

export type ConversationCreateRequest = ConversationRequestBase;
export type ConversationUpdateRequest = ConversationRequestBase & { conversation: { id: string } };

type CompatibleJSONSchema = Exclude<JSONSchema, boolean>;

export interface ContextDefinition {
name: string;
description: string;
}

interface FunctionResponse {
content?: Serializable;
data?: Serializable;
}

interface FunctionOptions<TParameters extends CompatibleJSONSchema = CompatibleJSONSchema> {
name: string;
description: string;
parameters: TParameters;
contexts: string[];
}

type RespondFunction<
TParameters extends CompatibleJSONSchema,
TResponse extends FunctionResponse
> = (options: { arguments: FromSchema<TParameters> }, signal: AbortSignal) => Promise<TResponse>;

type RenderFunction<TResponse extends FunctionResponse> = (options: {
response: TResponse;
}) => React.ReactNode;

export interface FunctionDefinition {
options: FunctionOptions;
respond: (options: { arguments: any }, signal: AbortSignal) => Promise<FunctionResponse>;
render?: RenderFunction<any>;
}

export type RegisterContextDefinition = (options: ContextDefinition) => void;

export type RegisterFunctionDefinition = <
TParameters extends CompatibleJSONSchema,
TResponse extends FunctionResponse
>(
options: FunctionOptions<TParameters>,
respond: RespondFunction<TParameters, TResponse>,
render?: RenderFunction<TResponse>
) => void;

export type ContextRegistry = Map<string, ContextDefinition>;
export type FunctionRegistry = Map<string, FunctionDefinition>;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import { ComponentStory } from '@storybook/react';
import React from 'react';
import { Observable } from 'rxjs';
import { ObservabilityAIAssistantService } from '../../types';
import { ChatBody as Component } from './chat_body';

export default {
Expand Down Expand Up @@ -54,13 +56,11 @@ const defaultProps: ChatBodyProps = {
currentUser: {
username: 'elastic',
},
chat: {
loading: false,
abort: () => {},
generate: async () => {
return {} as any;
service: {
chat: () => {
return new Observable();
},
},
} as unknown as ObservabilityAIAssistantService,
};

export const ChatBody = Template.bind({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { css } from '@emotion/css';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import React from 'react';
import { type ConversationCreateRequest } from '../../../common/types';
import type { UseChatResult } from '../../hooks/use_chat';
import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors';
import { useTimeline } from '../../hooks/use_timeline';
import { ObservabilityAIAssistantService } from '../../types';
import { HideExpandConversationListButton } from '../buttons/hide_expand_conversation_list_button';
import { ChatHeader } from './chat_header';
import { ChatPromptEditor } from './chat_prompt_editor';
Expand All @@ -30,14 +30,14 @@ export function ChatBody({
initialConversation,
connectors,
currentUser,
chat,
service,
isConversationListExpanded,
onToggleExpandConversationList,
}: {
initialConversation?: ConversationCreateRequest;
connectors: UseGenAIConnectorsResult;
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'>;
chat: UseChatResult;
service: ObservabilityAIAssistantService;
isConversationListExpanded?: boolean;
onToggleExpandConversationList?: () => void;
}) {
Expand All @@ -47,7 +47,7 @@ export function ChatBody({
initialConversation,
connectors,
currentUser,
chat,
service,
});

return (
Expand Down Expand Up @@ -93,7 +93,7 @@ export function ChatBody({
<EuiFlexItem grow={false}>
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="m">
<ChatPromptEditor
loading={chat.loading}
loading={false}
disabled={!connectors.selectedConnector}
onSubmit={timeline.onSubmit}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import React, { useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFlyout, useEuiTheme } from '@elastic/eui';
import type { ConversationCreateRequest } from '../../../common/types';
import { useChat } from '../../hooks/use_chat';
import { useCurrentUser } from '../../hooks/use_current_user';
import { useGenAIConnectors } from '../../hooks/use_genai_connectors';
import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant';
import { ChatBody } from './chat_body';
import { ConversationList } from './conversation_list';

Expand All @@ -22,18 +22,20 @@ export function ChatFlyout({
isOpen: boolean;
onClose: () => void;
}) {
const currentUser = useCurrentUser();
const connectors = useGenAIConnectors();
const { euiTheme } = useEuiTheme();

const chat = useChat();
const currentUser = useCurrentUser();

const { euiTheme } = useEuiTheme();

const [isConversationListExpanded, setIsConversationListExpanded] = useState(false);

const handleClickConversation = (id: string) => {};
const handleClickNewChat = () => {};
const handleClickSettings = () => {};

const service = useObservabilityAIAssistant();

return isOpen ? (
<EuiFlyout onClose={onClose}>
<EuiFlexGroup responsive={false} gutterSize="none">
Expand All @@ -51,7 +53,7 @@ export function ChatFlyout({
) : null}
<EuiFlexItem>
<ChatBody
chat={chat}
service={service}
connectors={connectors}
initialConversation={initialConversation}
currentUser={currentUser}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
EuiFlexItem,
EuiPopover,
} from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { MessageRole } from '../../../common/types';
Expand All @@ -40,6 +41,16 @@ export interface ChatItemProps extends ChatTimelineItem {
onStopGeneratingClick: () => void;
}

const euiCommentClassName = css`
.euiCommentEvent__headerEvent {
flex-grow: 1;
}

> div:last-child {
overflow: hidden;
}
`;

export function ChatItem({
title,
content,
Expand Down Expand Up @@ -143,15 +154,16 @@ export function ChatItem({
title={title}
/>
}
timelineAvatar={<ChatItemAvatar currentUser={currentUser} role={role} />}
className={euiCommentClassName}
timelineAvatar={
<ChatItemAvatar loading={loading && !content} currentUser={currentUser} role={role} />
}
username={getRoleTranslation(role)}
>
{content !== undefined || error || loading ? (
{content || error || loading || controls ? (
<MessagePanel
body={
content !== undefined || loading ? (
<MessageText content={content || ''} loading={loading} />
) : null
content || loading ? <MessageText content={content || ''} loading={loading} /> : null
}
error={error}
controls={controls}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,19 @@ import { MessageRole } from '../../../common/types';
interface ChatAvatarProps {
currentUser?: Pick<AuthenticatedUser, 'full_name' | 'username'> | undefined;
role: MessageRole;
loading: boolean;
}

export function ChatItemAvatar({ currentUser, role }: ChatAvatarProps) {
export function ChatItemAvatar({ currentUser, role, loading }: ChatAvatarProps) {
const isLoading = loading || !currentUser;

if (isLoading) {
return <EuiLoadingSpinner size="xl" />;
}

switch (role) {
case MessageRole.User:
return currentUser ? (
<UserAvatar user={currentUser} size="m" data-test-subj="userMenuAvatar" />
) : (
<EuiLoadingSpinner size="xl" />
);
return <UserAvatar user={currentUser} size="m" data-test-subj="userMenuAvatar" />;

case MessageRole.Assistant:
case MessageRole.Elastic:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ import {
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useFunctions, Func } from '../../hooks/use_functions';
import { useFunctions, type Func } from '../../hooks/use_functions';
import { type Message, MessageRole } from '../../../common';

export interface ChatPromptEditorProps {
disabled: boolean;
loading: boolean;
onSubmit: (message: {
content?: string;
function_call?: { name: string; args?: string };
}) => Promise<void>;
onSubmit: (message: Message) => Promise<void>;
}

export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEditorProps) {
Expand All @@ -40,7 +38,10 @@ export function ChatPromptEditor({ onSubmit, disabled, loading }: ChatPromptEdit
const handleSubmit = () => {
const currentPrompt = prompt;
setPrompt('');
onSubmit({ content: currentPrompt })
onSubmit({
'@timestamp': new Date().toISOString(),
message: { role: MessageRole.User, content: currentPrompt },
})
.then(() => {
setPrompt('');
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,17 @@
* 2.0.
*/

import React from 'react';
import { EuiCommentList } from '@elastic/eui';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { MessageRole } from '../../../common/types';
import React from 'react';
import type { Message } from '../../../common';
import type { Feedback } from '../feedback_buttons';
import { ChatItem } from './chat_item';

export interface ChatTimelineItem {
export interface ChatTimelineItem
extends Pick<Message['message'], 'role' | 'content' | 'function_call'> {
id: string;
title: string;
role: MessageRole;
content?: string;
function_call?: {
name: string;
args?: string;
trigger?: MessageRole;
};
loading: boolean;
error?: any;
canEdit: boolean;
Expand Down
Loading