diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 8b055b0a3776c..0fad443871add 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -22,7 +22,6 @@ export interface Message { message: { content?: string; name?: string; - event?: string; role: MessageRole; function_call?: { name: string; diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts index 216a748ff5086..250c870883c37 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_timeline.ts @@ -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 { @@ -85,7 +86,7 @@ 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; @@ -93,6 +94,14 @@ export function useTimeline({ 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, @@ -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 ); @@ -164,7 +177,7 @@ export function useTimeline({ name, content: JSON.stringify({ message: error.toString(), - ...error.body, + error: error.body, }), }, }) diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts index d3e85d604a270..bb3d3111b43be 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.ts @@ -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>({ diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx index 6ed99216f672b..402109bd05c1c 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.tsx @@ -6,6 +6,7 @@ */ 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'; @@ -13,11 +14,32 @@ 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({ @@ -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', { @@ -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; }