Skip to content

Commit

Permalink
Timeline fixes (elastic#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgieselaar committed Aug 10, 2023
1 parent ca13be0 commit 8797116
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 69 deletions.
1 change: 0 additions & 1 deletion x-pack/plugins/observability_ai_assistant/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export interface Message {
message: {
content?: string;
name?: string;
event?: string;
role: MessageRole;
function_call?: {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { AbortError } from '@kbn/kibana-utils-plugin/common';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { last } from 'lodash';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { Subscription } from 'rxjs';
import {
Expand Down Expand Up @@ -85,14 +86,22 @@ export function useTimeline({
function chat(nextMessages: Message[]): Promise<Message[]> {
const controller = new AbortController();

return new Promise<PendingMessage>((resolve, reject) => {
return new Promise<PendingMessage | undefined>((resolve, reject) => {
if (!connectorId) {
reject(new Error('Can not add a message without a connector'));
return;
}

onChatUpdate(nextMessages);

const lastMessage = last(nextMessages);

if (lastMessage?.message.function_call?.name) {
// the user has edited a function suggestion, no need to talk to
resolve(undefined);
return;
}

const response$ = chatService!.chat({
messages: nextMessages,
connectorId,
Expand All @@ -116,31 +125,35 @@ export function useTimeline({
return nextSubscription;
});
}).then(async (reply) => {
if (reply.error) {
if (reply?.error) {
return nextMessages;
}
if (reply.aborted) {
if (reply?.aborted) {
return nextMessages;
}

setPendingMessage(undefined);

const messagesAfterChat = nextMessages.concat({
'@timestamp': new Date().toISOString(),
message: {
...reply.message,
},
});
const messagesAfterChat = reply
? nextMessages.concat({
'@timestamp': new Date().toISOString(),
message: {
...reply.message,
},
})
: nextMessages;

onChatUpdate(messagesAfterChat);

if (reply?.message.function_call?.name) {
const name = reply.message.function_call.name;
const lastMessage = last(messagesAfterChat);

if (lastMessage?.message.function_call?.name) {
const name = lastMessage.message.function_call.name;

try {
const message = await chatService!.executeFunction(
name,
reply.message.function_call.arguments,
lastMessage.message.function_call.arguments,
controller.signal
);

Expand All @@ -164,7 +177,7 @@ export function useTimeline({
name,
content: JSON.stringify({
message: error.toString(),
...error.body,
error: error.body,
}),
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export async function createChatService({
getContexts,
getFunctions,
hasRenderFunction: (name: string) => {
return getFunctions().some((fn) => fn.options.name === name);
return !!getFunctions().find((fn) => fn.options.name === name)?.render;
},
chat({ connectorId, messages }: { connectorId: string; messages: Message[] }) {
const subject = new BehaviorSubject<PendingMessage>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,40 @@
*/
import { i18n } from '@kbn/i18n';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { isEmpty, omitBy } from 'lodash';
import React from 'react';
import { v4 } from 'uuid';
import { Message, MessageRole } from '../../common';
import type { ChatTimelineItem } from '../components/chat/chat_timeline';
import { RenderFunction } from '../components/render_function';
import type { ObservabilityAIAssistantChatService } from '../types';

function convertFunctionParamsToMarkdownCodeBlock(object: Record<string, string | number>) {
return `
\`\`\`
${JSON.stringify(object, null, 4)}
\`\`\``;
function convertMessageToMarkdownCodeBlock(message: Message['message']) {
let value: object;

if (!message.name) {
const name = message.function_call?.name;
const args = message.function_call?.arguments
? JSON.parse(message.function_call.arguments)
: undefined;

value = {
name,
args,
};
} else {
const content = message.content ? JSON.parse(message.content) : undefined;
const data = message.data ? JSON.parse(message.data) : undefined;
value = omitBy(
{
content,
data,
},
isEmpty
);
}

return `\`\`\`\n${JSON.stringify(value, null, 2)}\n\`\`\``;
}

export function getTimelineItemsfromConversation({
Expand Down Expand Up @@ -73,50 +95,59 @@ export function getTimelineItemsfromConversation({
break;

case MessageRole.User:
canCopy = true;
canGiveFeedback = false;
canRegenerate = false;
hide = false;
// User executed a function:
if (message.message.name && functionCall) {
title = i18n.translate('xpack.observabilityAiAssistant.executedFunctionEvent', {
defaultMessage: 'executed the function {functionName}',
values: {
functionName: message.message.name,
},
});

content = convertFunctionParamsToMarkdownCodeBlock({
name: message.message.name,
arguments: JSON.parse(functionCall.arguments || '{}'),
});

element = chatService.hasRenderFunction(message.message.name) ? (
<RenderFunction
name={message.message.name}
arguments={functionCall?.arguments}
response={message.message}
/>
) : null;
if (message.message.name && functionCall) {
const parsedContent = JSON.parse(message.message.content ?? 'null');
const isError = !!(parsedContent && 'error' in parsedContent);

title = !isError
? i18n.translate('xpack.observabilityAiAssistant.executedFunctionEvent', {
defaultMessage: 'executed the function {functionName}',
values: {
functionName: message.message.name,
},
})
: i18n.translate('xpack.observabilityAiAssistant.executedFunctionFailureEvent', {
defaultMessage: 'failed to execute the function {functionName}',
values: {
functionName: message.message.name,
},
});

element =
!isError && chatService.hasRenderFunction(message.message.name) ? (
<RenderFunction
name={message.message.name}
arguments={functionCall?.arguments}
response={message.message}
/>
) : undefined;

content = !element ? convertMessageToMarkdownCodeBlock(message.message) : undefined;

canCopy = true;
canEdit = hasConnector;
canGiveFeedback = true;
canRegenerate = hasConnector;
collapsed = !Boolean(element);
hide = false;
canEdit = false;
collapsed = !isError && !element;
} else {
// is a prompt by the user
title = '';
content = message.message.content;

canCopy = true;
canEdit = hasConnector;
canGiveFeedback = false;
canRegenerate = false;
collapsed = false;
hide = false;
}

break;

case MessageRole.Assistant:
canRegenerate = hasConnector;
canCopy = true;
canGiveFeedback = true;
hide = false;
// is a function suggestion by the assistant
if (!!functionCall?.name) {
title = i18n.translate('xpack.observabilityAiAssistant.suggestedFunctionEvent', {
Expand All @@ -125,32 +156,16 @@ export function getTimelineItemsfromConversation({
functionName: functionCall!.name,
},
});
content =
i18n.translate('xpack.observabilityAiAssistant.responseWas', {
defaultMessage: 'Suggested the payload: ',
}) +
convertFunctionParamsToMarkdownCodeBlock({
name: functionCall!.name,
arguments: JSON.parse(functionCall?.arguments || '{}'),
});

canCopy = true;
canEdit = false;
canGiveFeedback = true;
canRegenerate = false;
content = convertMessageToMarkdownCodeBlock(message.message);

collapsed = true;
hide = false;
canEdit = true;
} else {
// is an assistant response
title = '';
content = message.message.content;

canCopy = true;
canEdit = false;
canGiveFeedback = true;
canRegenerate = hasConnector;
collapsed = false;
hide = false;
canEdit = false;
}
break;
}
Expand Down

0 comments on commit 8797116

Please sign in to comment.