From ca5726861a5d4f85225419b022e54c17d5782877 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 30 Jan 2024 14:13:44 +0100 Subject: [PATCH 01/17] Add chatContext functionality --- .../common/functions/lens.tsx | 2 +- .../common/types.ts | 7 ++ .../public/components/chat/chat_body.test.tsx | 8 +- .../public/hooks/use_chat.test.ts | 3 + .../public/hooks/use_chat.ts | 17 ++-- .../public/hooks/use_conversation.ts | 1 + ...observability_ai_assistant_chat_context.ts | 15 ++++ .../public/mock.tsx | 6 ++ .../public/plugin.tsx | 2 + .../service/create_chat_service.test.ts | 2 +- .../public/service/create_chat_service.ts | 79 ++++++++++--------- .../public/service/create_service.ts | 15 ++++ .../public/types.ts | 16 ++-- ..._timeline_items_from_conversation.test.tsx | 12 +-- .../functions/{recall.ts => context.ts} | 49 ++++++------ .../server/functions/index.ts | 10 +-- .../server/routes/chat/route.ts | 12 ++- .../server/routes/runtime_types.ts | 9 +++ .../server/service/client/index.test.ts | 12 +-- .../server/service/client/index.ts | 23 +++++- 20 files changed, 196 insertions(+), 104 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_context.ts rename x-pack/plugins/observability_ai_assistant/server/functions/{recall.ts => context.ts} (85%) diff --git a/x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx b/x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx index a3d8487a83b8a..475c99faf1ddf 100644 --- a/x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx +++ b/x-pack/plugins/observability_ai_assistant/common/functions/lens.tsx @@ -24,7 +24,7 @@ export const lensFunctionDefinition = { name: 'lens', contexts: ['core'], description: - "Use this function to create custom visualizations, using Lens, that can be saved to dashboards. This function does not return data to the assistant, it only shows it to the user. When using this function, make sure to use the recall function to get more information about how to use it, with how you want to use it. Make sure the query also contains information about the user's request. The visualisation is displayed to the user above your reply, DO NOT try to generate or display an image yourself.", + "Use this function to create custom visualizations, using Lens, that can be saved to dashboards. This function does not return data to the assistant, it only shows it to the user. When using this function, make sure to use the context function to get more information about how to use it, with how you want to use it. Make sure the query also contains information about the user's request. The visualisation is displayed to the user above your reply, DO NOT try to generate or display an image yourself.", descriptionForUser: 'Use this function to create custom visualizations, using Lens, that can be saved to dashboards.', parameters: { diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 1bbaf198603d8..6026707e315cf 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -125,3 +125,10 @@ export type RegisterContextDefinition = (options: ContextDefinition) => void; export type ContextRegistry = Map; export type FunctionRegistry = Map; + +export interface ChatContext { + [key: string]: { + value: string | number; + description: string; + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx index 23e1e61e16dc7..2be9586aa33ed 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx @@ -38,7 +38,7 @@ describe('', () => { message: { role: 'assistant', function_call: { - name: 'recall', + name: 'context', arguments: '{"queries":[],"contexts":[]}', trigger: 'assistant', }, @@ -48,7 +48,7 @@ describe('', () => { { message: { role: 'user', - name: 'recall', + name: 'context', content: '[]', }, }, @@ -86,7 +86,7 @@ describe('', () => { message: { role: 'assistant', function_call: { - name: 'recall', + name: 'context', arguments: '{"queries":[],"contexts":[]}', trigger: 'assistant', }, @@ -96,7 +96,7 @@ describe('', () => { { message: { role: 'user', - name: 'recall', + name: 'context', content: '[]', }, }, diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.test.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.test.ts index a5f06d3a27c98..2cc37ea2780f9 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.test.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.test.ts @@ -13,6 +13,7 @@ import { StreamingChatResponseEventType, StreamingChatResponseEventWithoutError, } from '../../common/conversation_complete'; +import { mockService } from '../mock'; import type { ObservabilityAIAssistantChatService } from '../types'; import { ChatState, useChat, type UseChatProps, type UseChatResult } from './use_chat'; import * as useKibanaModule from './use_kibana'; @@ -69,6 +70,7 @@ describe('useChat', () => { }, ], persist: false, + service: mockService, } as UseChatProps, }); }); @@ -95,6 +97,7 @@ describe('useChat', () => { chatService: mockChatService, initialMessages: [], persist: false, + service: mockService, } as UseChatProps, }); diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts index d68bf549fd9b2..783f0c2898c5d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts @@ -16,7 +16,10 @@ import { StreamingChatResponseEventType, } from '../../common/conversation_complete'; import { getAssistantSetupMessage } from '../service/get_assistant_setup_message'; -import type { ObservabilityAIAssistantChatService } from '../types'; +import type { + ObservabilityAIAssistantChatService, + ObservabilityAIAssistantService, +} from '../types'; import { useKibana } from './use_kibana'; import { useOnce } from './use_once'; @@ -45,6 +48,7 @@ export interface UseChatResult { export interface UseChatProps { initialMessages: Message[]; initialConversationId?: string; + service: ObservabilityAIAssistantService; chatService: ObservabilityAIAssistantChatService; connectorId?: string; persist: boolean; @@ -55,6 +59,7 @@ export interface UseChatProps { export function useChat({ initialMessages, initialConversationId, + service, chatService, connectorId, onConversationUpdate, @@ -130,6 +135,7 @@ export function useChat({ setChatState(ChatState.Loading); const next$ = chatService.complete({ + chatContext: service.getChatContext(), connectorId, messages: getWithSystemMessage(nextMessages, systemMessage), persist, @@ -224,13 +230,14 @@ export function useChat({ }); }, [ - connectorId, chatService, - handleSignalAbort, - systemMessage, + connectorId, + conversationId, handleError, + handleSignalAbort, persist, - conversationId, + service, + systemMessage, ] ); diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts index e9d5b3f8073e4..df3c59a316ad7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.ts @@ -105,6 +105,7 @@ export function useConversation({ initialMessages, initialConversationId, chatService, + service, connectorId, onConversationUpdate: (event) => { setDisplayedConversationId(event.conversation.id); diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_context.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_context.ts new file mode 100644 index 0000000000000..4b6bb5eaf61d8 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_context.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ObservabilityAIAssistantService } from '../types'; + +export function useObservabilityAIAssistantChatContext(service: ObservabilityAIAssistantService) { + return { + setContext: service.setChatContext, + getContext: service.getChatContext, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/mock.tsx b/x-pack/plugins/observability_ai_assistant/public/mock.tsx index 7263fe6c40a3a..e331e59bb5c89 100644 --- a/x-pack/plugins/observability_ai_assistant/public/mock.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/mock.tsx @@ -64,6 +64,8 @@ export const mockService: ObservabilityAIAssistantService = { navigate: () => {}, } as unknown as SharePluginStart), register: () => {}, + setChatContext: () => {}, + getChatContext: () => ({}), }; function createSetupContract(): ObservabilityAIAssistantPluginSetup { @@ -87,6 +89,10 @@ function createStartContract(): ObservabilityAIAssistantPluginStart { selectConnector: () => {}, reloadConnectors: () => {}, }), + useObservabilityAIAssistantChatContext: () => ({ + setChatContext: () => {}, + getChatContext: () => ({}), + }), }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx index 5ee13a1c5b6e8..888da2b5dab4a 100644 --- a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx @@ -21,6 +21,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { withSuspense } from '@kbn/shared-ux-utility'; import { createService } from './service/create_service'; import { useGenAIConnectorsWithoutContext } from './hooks/use_genai_connectors'; +import { useObservabilityAIAssistantChatContext } from './hooks/use_observability_ai_assistant_chat_context'; import type { ConfigSchema, ObservabilityAIAssistantPluginSetup, @@ -146,6 +147,7 @@ export class ObservabilityAIAssistantPlugin return { service, useGenAIConnectors: () => useGenAIConnectorsWithoutContext(service), + useObservabilityAIAssistantChatContext: () => useObservabilityAIAssistantChatContext(service), ObservabilityAIAssistantContextualInsight: isEnabled ? withSuspense( withProviders( diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts index b5b86fa4f15b3..4cfcb5674e4ad 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_chat_service.test.ts @@ -57,7 +57,7 @@ describe('createChatService', () => { } function chat({ signal }: { signal: AbortSignal } = { signal: new AbortController().signal }) { - return service.chat({ signal, messages: [], connectorId: '' }); + return service.chat({ signal, messages: [], connectorId: '', chatContext: {} }); } beforeEach(async () => { 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 06b521ca04550..c4679b6a57e04 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 @@ -19,6 +19,7 @@ import { type FunctionRegistry, type FunctionResponse, type Message, + type ChatContext, } from '../../common/types'; import { filterFunctionDefinitions } from '../../common/utils/filter_function_definitions'; import { throwSerializedChatCompletionErrors } from '../../common/utils/throw_serialized_chat_completion_errors'; @@ -140,49 +141,17 @@ export async function createChatService({ hasRenderFunction: (name: string) => { return renderFunctionRegistry.has(name); }, - complete({ connectorId, messages, conversationId, persist, signal }) { - return new Observable((subscriber) => { - client('POST /internal/observability_ai_assistant/chat/complete', { - params: { - body: { - messages, - connectorId, - conversationId, - persist, - }, - }, - signal, - asResponse: true, - rawResponse: true, - }) - .then((_response) => { - const response = _response as unknown as HttpResponse; - const response$ = toObservable(response) - .pipe( - map((line) => JSON.parse(line) as StreamingChatResponseEvent), - throwSerializedChatCompletionErrors() - ) - .subscribe(subscriber); - - signal.addEventListener('abort', () => { - response$.unsubscribe(); - }); - }) - .catch((err) => { - subscriber.error(err); - subscriber.complete(); - }); - }); - }, chat({ + chatContext, connectorId, - messages, function: callFunctions = 'auto', + messages, signal, }: { connectorId: string; - messages: Message[]; + chatContext: ChatContext; function?: 'none' | 'auto'; + messages: Message[]; signal: AbortSignal; }) { return new Observable((subscriber) => { @@ -193,8 +162,9 @@ export async function createChatService({ client('POST /internal/observability_ai_assistant/chat', { params: { body: { - messages, + chatContext, connectorId, + messages, functions: callFunctions === 'none' ? [] @@ -244,5 +214,40 @@ export async function createChatService({ shareReplay() ); }, + complete({ chatContext, connectorId, conversationId, messages, persist, signal }) { + return new Observable((subscriber) => { + client('POST /internal/observability_ai_assistant/chat/complete', { + params: { + body: { + chatContext, + connectorId, + conversationId, + messages, + persist, + }, + }, + signal, + asResponse: true, + rawResponse: true, + }) + .then((_response) => { + const response = _response as unknown as HttpResponse; + const response$ = toObservable(response) + .pipe( + map((line) => JSON.parse(line) as StreamingChatResponseEvent), + throwSerializedChatCompletionErrors() + ) + .subscribe(subscriber); + + signal.addEventListener('abort', () => { + response$.unsubscribe(); + }); + }) + .catch((err) => { + subscriber.error(err); + subscriber.complete(); + }); + }); + }, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index 721f13234591b..36e9a0200a06b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -11,6 +11,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { createCallObservabilityAIAssistantAPI } from '../api'; import type { ChatRegistrationRenderFunction, ObservabilityAIAssistantService } from '../types'; +import type { ChatContext } from '../../common/types'; export function createService({ analytics, @@ -31,6 +32,13 @@ export function createService({ const registrations: ChatRegistrationRenderFunction[] = []; + let chatContext: ChatContext = { + url: { + value: window.location.href, + description: 'The URL that the user is currently looking at', + }, + }; + return { isEnabled: () => { return enabled; @@ -46,5 +54,12 @@ export function createService({ getCurrentUser: () => securityStart.authc.getCurrentUser(), getLicense: () => licenseStart.license$, getLicenseManagementLocator: () => shareStart, + setChatContext: (newChatContext: ChatContext) => { + chatContext = { + ...chatContext, + ...newChatContext, + }; + }, + getChatContext: () => chatContext, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index d535a7cfae27a..9fd8d95a3ac59 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -5,6 +5,8 @@ * 2.0. */ +import type { ForwardRefExoticComponent, RefAttributes } from 'react'; +import type { Observable } from 'rxjs'; import type { AnalyticsServiceStart } from '@kbn/core/public'; import type { DataViewsPublicPluginSetup, @@ -29,10 +31,9 @@ import type { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '@kbn/triggers-actions-ui-plugin/public'; -import { ForwardRefExoticComponent, RefAttributes } from 'react'; -import type { Observable } from 'rxjs'; import type { StreamingChatResponseEventWithoutError } from '../common/conversation_complete'; import type { + ChatContext, ContextDefinition, FunctionDefinition, FunctionResponse, @@ -51,16 +52,18 @@ export type { PendingMessage }; export interface ObservabilityAIAssistantChatService { analytics: AnalyticsServiceStart; chat: (options: { - messages: Message[]; + chatContext: ChatContext; connectorId: string; function?: 'none' | 'auto'; + messages: Message[]; signal: AbortSignal; }) => Observable; complete: (options: { - messages: Message[]; + chatContext: ChatContext; + conversationId?: string; connectorId: string; + messages: Message[]; persist: boolean; - conversationId?: string; signal: AbortSignal; }) => Observable; getContexts: () => ContextDefinition[]; @@ -82,6 +85,8 @@ export interface ObservabilityAIAssistantService { getLicenseManagementLocator: () => SharePluginStart; start: ({}: { signal: AbortSignal }) => Promise; register: (fn: ChatRegistrationRenderFunction) => void; + setChatContext: (newChatContext: ChatContext) => void; + getChatContext: () => ChatContext; } export type RenderFunction = (options: { @@ -132,4 +137,5 @@ export interface ObservabilityAIAssistantPluginStart { RefAttributes<{}> > | null; useGenAIConnectors: () => UseGenAIConnectorsResult; + useObservabilityAIAssistantChatContext: () => {}; } diff --git a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx index 510e09705888d..223dbbdde1c90 100644 --- a/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/utils/get_timeline_items_from_conversation.test.tsx @@ -114,7 +114,7 @@ describe('getTimelineItemsFromConversation', () => { message: { role: MessageRole.Assistant, function_call: { - name: 'recall', + name: 'context', arguments: JSON.stringify({ queries: [], contexts: [] }), trigger: MessageRole.Assistant, }, @@ -124,7 +124,7 @@ describe('getTimelineItemsFromConversation', () => { '@timestamp': new Date().toISOString(), message: { role: MessageRole.User, - name: 'recall', + name: 'context', content: JSON.stringify([]), }, }, @@ -155,7 +155,7 @@ describe('getTimelineItemsFromConversation', () => { ), }); - expect(container.textContent).toBe('requested the function recall'); + expect(container.textContent).toBe('requested the function context'); }); it('formats the function response', () => { @@ -181,7 +181,7 @@ describe('getTimelineItemsFromConversation', () => { ), }); - expect(container.textContent).toBe('executed the function recall'); + expect(container.textContent).toBe('executed the function context'); }); }); describe('with a render function', () => { @@ -453,7 +453,7 @@ describe('getTimelineItemsFromConversation', () => { message: { role: MessageRole.Assistant, function_call: { - name: 'recall', + name: 'context', arguments: JSON.stringify({ queries: [], contexts: [] }), trigger: MessageRole.User, }, @@ -463,7 +463,7 @@ describe('getTimelineItemsFromConversation', () => { '@timestamp': new Date().toISOString(), message: { role: MessageRole.User, - name: 'recall', + name: 'context', content: JSON.stringify([]), }, }, diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts similarity index 85% rename from x-pack/plugins/observability_ai_assistant/server/functions/recall.ts rename to x-pack/plugins/observability_ai_assistant/server/functions/context.ts index b4b684f3d3930..88fa4197e9bde 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/recall.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts @@ -16,38 +16,32 @@ import { MessageRole, type Message } from '../../common/types'; import { concatenateChatCompletionChunks } from '../../common/utils/concatenate_chat_completion_chunks'; import type { ObservabilityAIAssistantClient } from '../service/client'; -export function registerRecallFunction({ +export function registerContextFunction({ client, registerFunction, resources, }: FunctionRegistrationParameters) { registerFunction( { - name: 'recall', + name: 'context', contexts: ['core'], - description: `Use this function to recall earlier learnings. Anything you will summarize can be retrieved again later via this function. - - The learnings are sorted by score, descending. - + description: + dedent(`Use this function to recall earlier learnings and get the context in which the user is currently using Kibana. + + Anything this is summarized can be retrieved again later via this function. The learnings are sorted by score, descending. + Make sure the query covers ONLY the following aspects: - - Anything you've inferred from the user's request, but is not mentioned in the user's request - - The functions you think might be suitable for answering the user's request. If there are multiple functions that seem suitable, create multiple queries. Use the function name in the query. - - DO NOT include the user's request. It will be added internally. - - The user asks: "can you visualise the average request duration for opbeans-go over the last 7 days?" - You recall: { - "queries": [ - "APM service, - "lens function usage", - "get_apm_timeseries function usage" - ], - "contexts": [ - "lens", - "apm" - ] - }`, - descriptionForUser: 'This function allows the assistant to recall previous learnings.', + - Anything you've inferred from the user's request but is not mentioned in the user's request + - The functions you think might be suitable for answering the user's request. If there are multiple functions that seem suitable, create multiple queries. Use the function name in the query. DO NOT include the user's request. It will be added internally. + + Use this function to get the context of the application that the user is currently using. Examples of context are: + - the URL the user is at; + - the time range the user is looking at; + - the service the user is looking at. + + This context can change every time the user adds a prompt, so you should call this function every time you need the context.`), + descriptionForUser: + 'This function allows the assistant to recall previous learnings from the Knowledge base and gather context of how you are using the application.', parameters: { type: 'object', additionalProperties: false, @@ -83,6 +77,8 @@ export function registerRecallFunction({ throw new Error('No system message found'); } + const chatContext = await client.getChatContext(); + const userMessage = last( messages.filter((message) => message.message.role === MessageRole.User) ); @@ -101,7 +97,7 @@ export function registerRecallFunction({ if (suggestions.length === 0) { return { - content: [] as unknown as Serializable, + content: { learnings: [] as unknown as Serializable, chatContext }, }; } @@ -119,7 +115,7 @@ export function registerRecallFunction({ resources.logger.debug(JSON.stringify(relevantDocuments, null, 2)); return { - content: relevantDocuments as unknown as Serializable, + content: { learnings: relevantDocuments as unknown as Serializable, chatContext }, }; } ); @@ -130,7 +126,6 @@ async function retrieveSuggestions({ queries, client, contexts, - signal, }: { userMessage?: Message; queries: string[]; diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts index 12075a56942f6..89ad91880ac1a 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts @@ -6,7 +6,7 @@ */ import dedent from 'dedent'; -import { registerRecallFunction } from './recall'; +import { registerContextFunction } from './context'; import { registerSummarizationFunction } from './summarize'; import { ChatRegistrationFunction } from '../service/types'; import { registerAlertsFunction } from './alerts'; @@ -59,7 +59,7 @@ export const registerFunctions: ChatRegistrationFunction = async ({ Use the "get_dataset_info" function if it is not clear what fields or indices the user means, or if you want to get more information about the mappings. If the user asks about a query, or ES|QL, always call the "esql" function. DO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries or explain anything about the ES|QL query language yourself. - Even if the "recall" function was used before that, follow it up with the "esql" function. If a query fails, do not attempt to correct it yourself. Again you should call the "esql" function, + Even if the "context" function was used before that, follow it up with the "esql" function. If a query fails, do not attempt to correct it yourself. Again you should call the "esql" function, even if it has been called before. If the "get_dataset_info" function returns no data, and the user asks for a query, generate a query anyway with the "esql" function, but be explicit about it potentially being incorrect. @@ -69,15 +69,15 @@ export const registerFunctions: ChatRegistrationFunction = async ({ if (isReady) { description += `You can use the "summarize" functions to store new information you have learned in a knowledge database. Once you have established that you did not know the answer to a question, and the user gave you this information, it's important that you create a summarisation of what you have learned and store it in the knowledge database. Don't create a new summarization if you see a similar summarization in the conversation, instead, update the existing one by re-using its ID. - Additionally, you can use the "recall" function to retrieve relevant information from the knowledge database. + Additionally, you can use the "context" function to retrieve relevant information from the knowledge database. `; registerSummarizationFunction(registrationParameters); - registerRecallFunction(registrationParameters); + registerContextFunction(registrationParameters); registerLensFunction(registrationParameters); } else { - description += `You do not have a working memory. Don't try to recall information via the "recall" function. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base. A banner is available at the top of the conversation to set this up.`; + description += `You do not have a working memory. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base.`; } registerElasticsearchFunction(registrationParameters); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index 7cc57b769ece8..15e024a0995fd 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -10,7 +10,7 @@ import { toBooleanRt } from '@kbn/io-ts-utils'; import type OpenAI from 'openai'; import { Readable } from 'stream'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; -import { messageRt } from '../runtime_types'; +import { messageRt, chatContextRt } from '../runtime_types'; import { observableIntoStream } from '../../service/util/observable_into_stream'; const chatRoute = createObservabilityAIAssistantServerRoute({ @@ -21,8 +21,9 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ params: t.type({ body: t.intersection([ t.type({ - messages: t.array(messageRt), connectorId: t.string, + messages: t.array(messageRt), + chatContext: chatContextRt, functions: t.array( t.type({ name: t.string, @@ -41,6 +42,8 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ const client = await service.getClient({ request }); + client.setChatContext(params.body.chatContext); + if (!client) { throw notImplemented(); } @@ -80,6 +83,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ body: t.intersection([ t.type({ messages: t.array(messageRt), + chatContext: chatContextRt, connectorId: t.string, persist: toBooleanRt, }), @@ -114,7 +118,9 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ client, }); - const response$ = await client.complete({ + client.setChatContext(params.body.chatContext); + + const response$ = client.complete({ messages, connectorId, conversationId, diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts index 41d0d9d19492a..0ceaca4514883 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts @@ -13,6 +13,7 @@ import { ConversationUpdateRequest, Message, MessageRole, + ChatContext, } from '../../common/types'; const serializeableRt = t.any; @@ -92,3 +93,11 @@ export const conversationRt: t.Type = t.intersection([ }), }), ]); + +export const chatContextRt: t.Type = t.record( + t.string, + t.type({ + value: t.union([t.string, t.number]), + description: t.string, + }) +); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts index 2e88cb55f9dd5..f3671d56a5784 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts @@ -122,7 +122,7 @@ describe('Observability AI Assistant client', () => { functionClientMock.getFunctions.mockReturnValue([]); functionClientMock.hasFunction.mockImplementation((name) => { - return name !== 'recall'; + return name !== 'context'; }); currentUserEsClientMock.search.mockResolvedValue({ @@ -1066,7 +1066,7 @@ describe('Observability AI Assistant client', () => { }); }); - describe('when recall is available', () => { + describe('when context is available', () => { let stream: Readable; let dataHandler: jest.Mock; @@ -1119,7 +1119,7 @@ describe('Observability AI Assistant client', () => { await finished(stream); }); - it('appends the recall request message', () => { + it('appends the context request message', () => { expect(JSON.parse(dataHandler.mock.calls[0]!)).toEqual({ type: StreamingChatResponseEventType.MessageAdd, id: expect.any(String), @@ -1129,7 +1129,7 @@ describe('Observability AI Assistant client', () => { content: '', role: MessageRole.Assistant, function_call: { - name: 'recall', + name: 'context', arguments: JSON.stringify({ queries: [], contexts: [] }), trigger: MessageRole.Assistant, }, @@ -1138,7 +1138,7 @@ describe('Observability AI Assistant client', () => { }); }); - it('appends the recall response', () => { + it('appends the context response', () => { expect(JSON.parse(dataHandler.mock.calls[1]!)).toEqual({ type: StreamingChatResponseEventType.MessageAdd, id: expect.any(String), @@ -1147,7 +1147,7 @@ describe('Observability AI Assistant client', () => { message: { content: JSON.stringify([{ id: 'my_document', text: 'My document' }]), role: MessageRole.User, - name: 'recall', + name: 'context', }, }, }); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index e82ea13ece099..2e78972dfd8fc 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -27,6 +27,7 @@ import { import { FunctionResponse, MessageRole, + type ChatContext, type CompatibleJSONSchema, type Conversation, type ConversationCreateRequest, @@ -48,6 +49,8 @@ import { getAccessQuery } from '../util/get_access_query'; import { streamIntoObservable } from '../util/stream_into_observable'; export class ObservabilityAIAssistantClient { + chatContext: ChatContext = {}; + constructor( private readonly dependencies: { actionsClient: PublicMethodsOf; @@ -157,21 +160,21 @@ export class ObservabilityAIAssistantClient { const isUserMessageWithoutFunctionResponse = isUserMessage && !lastMessage?.message.name; - const recallFirst = - isUserMessageWithoutFunctionResponse && functionClient.hasFunction('recall'); + const contextFirst = + isUserMessageWithoutFunctionResponse && functionClient.hasFunction('context'); const isAssistantMessageWithFunctionRequest = lastMessage?.message.role === MessageRole.Assistant && !!lastMessage?.message.function_call?.name; - if (recallFirst) { + if (contextFirst) { const addedMessage = { '@timestamp': new Date().toISOString(), message: { role: MessageRole.Assistant, content: '', function_call: { - name: 'recall', + name: 'context', arguments: JSON.stringify({ queries: [], contexts: [], @@ -673,4 +676,16 @@ export class ObservabilityAIAssistantClient { deleteKnowledgeBaseEntry = async (id: string) => { return this.dependencies.knowledgeBaseService.deleteEntry({ id }); }; + + setChatContext = (newContext: ChatContext) => { + this.chatContext = { ...this.chatContext, ...newContext }; + }; + + getChatContext = async () => { + return this.chatContext; + }; + + clearChatContext = () => { + this.chatContext = {}; + }; } From d05bfcef3a1143b57015b4f4d6c87dc5f7b06ea1 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Tue, 30 Jan 2024 15:19:59 +0100 Subject: [PATCH 02/17] Correctly set URL --- .../components/action_menu_item/action_menu_item.tsx | 7 +++++++ .../public/components/insight/insight.tsx | 2 ++ .../public/service/create_service.ts | 7 +------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index 119a87554d30e..4a437cbd1f3e5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -34,6 +34,13 @@ export function ObservabilityAIAssistantActionMenuItem() { return null; } + service.setChatContext({ + url: { + value: window.location.href, + description: 'The URL that the user is currently looking at', + }, + }); + return ( <> { From 3018007dda008cb82aeb74618403e30375f6ee12 Mon Sep 17 00:00:00 2001 From: Coen Warmer Date: Wed, 7 Feb 2024 15:22:33 +0100 Subject: [PATCH 03/17] Add starter and contextual suggestions --- x-pack/plugins/apm/public/plugin.ts | 5 ++ x-pack/plugins/observability/public/plugin.ts | 5 ++ .../common/types.ts | 5 ++ .../public/components/chat/chat_body.tsx | 41 ++++++++- .../components/chat/welcome_message.test.tsx | 90 +++++++++++++++---- .../components/chat/welcome_message.tsx | 19 +++- .../suggestions/suggestion_button.tsx | 27 ++++++ .../components/suggestions/suggestions.tsx | 54 +++++++++++ .../components/technical_preview_badge.tsx | 27 ------ .../hooks/use_contextual_suggestions.ts | 76 ++++++++++++++++ .../public/service/create_chat_service.ts | 24 +++++ .../public/service/create_service.ts | 7 +- .../public/types.ts | 8 ++ .../server/routes/chat/route.ts | 2 - .../server/routes/conversations/route.ts | 49 +++++++++- .../server/service/client/index.ts | 32 +++++++ 16 files changed, 420 insertions(+), 51 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 476a4e1fd5b82..7148a8ed1f2b5 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -455,6 +455,11 @@ export class ApmPlugin implements Plugin { } ); + plugins.observabilityAIAssistant.service.registerStarterSuggestions([ + { app: 'apm', prompt: 'How are my services doing?' }, + { app: 'apm', prompt: 'Do my services have errors?' }, + ]); + if (fleet) { const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index d2dcf8c266976..e3b933ce7a4f2 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -454,6 +454,11 @@ export class Plugin ); }); + pluginsStart.observabilityAIAssistant.service.registerStarterSuggestions([ + { app: 'observability', prompt: 'How are my alerts doing?' }, + { app: 'observability', prompt: 'Can you help me set up an SLO?' }, + ]); + pluginsStart.observabilityShared.updateGlobalNavigation({ capabilities: application.capabilities, deepLinks: this.deepLinks, diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 296f08ae0552f..0cdf0c27dea9f 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -133,3 +133,8 @@ export interface ChatContext { description: string; }; } + +export interface Suggestion { + app?: string; + prompt: string; +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 373e35641ff8f..5fe8305fb52af 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -28,7 +28,7 @@ import { useLicense } from '../../hooks/use_license'; import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import { type Conversation, type Message, MessageRole } from '../../../common/types'; +import { type Conversation, type Message, MessageRole, Suggestion } from '../../../common/types'; import { ChatHeader } from './chat_header'; import { PromptEditor } from '../prompt_editor/prompt_editor'; import { ChatTimeline } from './chat_timeline'; @@ -43,6 +43,8 @@ import { import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { TELEMETRY, sendEvent } from '../../analytics'; +import { Suggestions } from '../suggestions/suggestions'; +import { useContextualSuggestions } from '../../hooks/use_contextual_suggestions'; const fullHeightClassName = css` height: 100%; @@ -120,6 +122,8 @@ export function ChatBody({ const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); + const { signal } = new AbortController(); + const chatService = useObservabilityAIAssistantChatService(); const { conversation, messages, next, state, stop, saveTitle } = useConversation({ @@ -161,6 +165,15 @@ export function ChatBody({ : '100%'}; `; + const suggestions = useContextualSuggestions({ + chatService, + connectors, + conversation, + messages, + signal, + state, + }); + const [stickToBottom, setStickToBottom] = useState(true); const isAtBottom = (parent: HTMLElement) => @@ -289,6 +302,17 @@ export function ChatBody({ } }; + const handleSuggestionClick = (suggestion: Suggestion) => { + next( + messages.concat([ + { + '@timestamp': new Date().toISOString(), + message: { role: MessageRole.User, content: suggestion.prompt }, + }, + ]) + ); + }; + if (!hasCorrectLicense && !initialConversationId) { footer = ( <> @@ -332,7 +356,11 @@ export function ChatBody({ className={animClassName} > {connectors.connectors?.length === 0 || messages.length === 1 ? ( - + ) : ( )} + {conversation && conversation.value && 'id' in conversation.value.conversation ? ( + + + + ) : null} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx index 56dd52c417a3b..b8e8c5cf5a0f6 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx @@ -119,7 +119,11 @@ describe('Welcome Message', () => { describe('when no connectors are available', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - + {}} + /> ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -127,7 +131,11 @@ describe('Welcome Message', () => { it('should show a set up connector button', () => { const { getByTestId } = render( - + {}} + /> ); expect( @@ -138,7 +146,11 @@ describe('Welcome Message', () => { describe('when no triggersactionsUi capabilities are available', () => { it('should navigate to stack management', () => { const { getByTestId } = render( - + {}} + /> ); fireEvent.click( @@ -180,7 +192,11 @@ describe('Welcome Message', () => { it('should render a connector flyout when clicking the set up connector button', () => { const { getByTestId } = render( - + {}} + /> ); fireEvent.click( @@ -219,7 +235,11 @@ describe('Welcome Message', () => { describe('when connectors are available', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - + {}} + /> ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -228,7 +248,11 @@ describe('Welcome Message', () => { describe('when knowledge base is not installed', () => { it('should render the retry and inspect errors buttons', () => { const { getByTestId } = render( - + {}} + /> ); expect( @@ -242,7 +266,11 @@ describe('Welcome Message', () => { it('should call kb install when clicking retry', async () => { const { getByTestId } = render( - + {}} + /> ); await act(async () => { @@ -255,7 +283,11 @@ describe('Welcome Message', () => { it('should render a popover with installation errors when clicking inspect', async () => { const { getByTestId } = render( - + {}} + /> ); fireEvent.click(getByTestId('observabilityAiAssistantWelcomeMessageInspectErrorsButton')); @@ -279,7 +311,11 @@ describe('Welcome Message', () => { it('should navigate to ML when clicking the link in the error popover', async () => { const { getByTestId } = render( - + {}} + /> ); fireEvent.click(getByTestId('observabilityAiAssistantWelcomeMessageInspectErrorsButton')); @@ -296,7 +332,11 @@ describe('Welcome Message', () => { describe('when knowledge base is installing', () => { it('should not show a failure message', () => { const { queryByTestId } = render( - + {}} + /> ); expect( @@ -310,7 +350,11 @@ describe('Welcome Message', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - + {}} + /> ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -320,7 +364,11 @@ describe('Welcome Message', () => { describe('when knowledge base is loading', () => { it('should not show a failure message', () => { const { queryByTestId } = render( - + {}} + /> ); expect( @@ -334,7 +382,11 @@ describe('Welcome Message', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - + {}} + /> ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -344,7 +396,11 @@ describe('Welcome Message', () => { describe('when knowledge base is installed', () => { it('should not show a failure message', () => { const { queryByTestId } = render( - + {}} + /> ); expect( @@ -358,7 +414,11 @@ describe('Welcome Message', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - + {}} + /> ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx index bf514691f7d93..8c63a07b2c78b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx @@ -25,6 +25,8 @@ import { Disclaimer } from './disclaimer'; import { WelcomeMessageConnectors } from './welcome_message_connectors'; import { WelcomeMessageKnowledgeBase } from './welcome_message_knowledge_base'; import { useKibana } from '../../hooks/use_kibana'; +import { Suggestions } from '../suggestions/suggestions'; +import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; const fullHeightClassName = css` height: 100%; @@ -38,10 +40,15 @@ const centerMaxWidthClassName = css` export function WelcomeMessage({ connectors, knowledgeBase, + onSelectSuggestion, }: { connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; + onSelectSuggestion: ({ prompt }: { prompt: string }) => void; }) { + const service = useObservabilityAIAssistant(); + + const suggestions = service.getStarterSuggestions(); const breakpoint = useCurrentEuiBreakpoint(); const { @@ -90,13 +97,13 @@ export function WelcomeMessage({ - +

{i18n.translate('xpack.observabilityAiAssistant.disclaimer.title', { defaultMessage: 'Welcome to the AI Assistant for Observability', @@ -112,6 +119,12 @@ export function WelcomeMessage({ /> + + + + + + diff --git a/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx new file mode 100644 index 0000000000000..94385c7bfab32 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import type { Suggestion } from '../../../common/types'; + +export function SuggestionButton({ + suggestion, + onSelect, +}: { + suggestion: Suggestion; + onSelect: ({ prompt }: { prompt: string }) => void; +}) { + const handleClick = () => onSelect({ prompt: suggestion.prompt }); + + return ( + + + {suggestion.prompt} + + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx b/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx new file mode 100644 index 0000000000000..5b101242c66ce --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/css'; +import { SuggestionButton } from './suggestion_button'; +import type { Suggestion } from '../../../common/types'; + +const GUTTER_SIZE = 's'; + +export function Suggestions({ + isLoading, + suggestions = [], + onSelect, +}: { + isLoading: boolean; + suggestions: Suggestion[]; + onSelect: ({ prompt }: { prompt: string }) => void; +}) { + const { euiTheme } = useEuiTheme(); + + const buttonContainerClassName = css` + min-width: calc(50% - ${euiTheme.size[GUTTER_SIZE]}); + max-width: calc(50% - ${euiTheme.size[GUTTER_SIZE]}); + `; + + const appName = window.location.pathname + .split('/') + .reduce((acc, pathPartial, index, arr) => (pathPartial === 'app' ? arr[index + 1] : acc), ''); + + const filteredSuggestions = useMemo(() => { + return suggestions.sort((a) => (a.app === appName ? -1 : 1)).slice(0, 4) || []; + }, [appName, suggestions]); + + return ( + + {isLoading ? ( + + + + ) : ( + filteredSuggestions.map((suggestion) => ( + + + + )) + )} + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx b/x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx deleted file mode 100644 index e1ddd86c2017c..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiBetaBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -export function TechnicalPreviewBadge() { - return ( - - ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts new file mode 100644 index 0000000000000..b492276ec16e8 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { ConversationRequestBase, Message } from '../../common/types'; +import { AbortableAsyncState, useAbortableAsync } from './use_abortable_async'; +import { ChatState } from './use_chat'; +import type { UseGenAIConnectorsResult } from './use_genai_connectors'; +import type { Conversation } from '../../common'; + +export function useContextualSuggestions({ + chatService, + connectors, + conversation, + messages, + state, + signal, +}: { + chatService: any; + connectors: UseGenAIConnectorsResult; + conversation: AbortableAsyncState; + messages: Message[]; + state: ChatState; + signal: AbortSignal | null; +}) { + const [lastMessageContent, setLastMessageContent] = useState( + messages[messages.length - 1]?.message.content + ); + + const [conversationId, setConversationId] = useState(''); + + const suggestions = useAbortableAsync(() => { + return connectors.selectedConnector && + conversation.value?.conversation && + 'id' in conversation.value.conversation + ? chatService.getContextualSuggestions({ + connectorId: connectors.selectedConnector, + conversationId: conversation.value.conversation.id, + signal, + }) + : Promise.resolve([]); + }, []); + + useEffect(() => { + if (state !== ChatState.Loading) { + if (lastMessageContent !== messages[messages.length - 1]?.message.content) { + setLastMessageContent(messages[messages.length - 1]?.message.content); + suggestions.refresh(); + } + } + if ( + conversation.value?.conversation && + 'id' in conversation.value.conversation && + conversationId !== conversation.value.conversation.id + ) { + setConversationId(conversation.value?.conversation.id); + suggestions.refresh(); + } + }, [ + conversation.value?.conversation, + conversationId, + lastMessageContent, + messages, + state, + suggestions, + ]); + + return { + value: suggestions.value, + loading: suggestions.loading, + }; +} 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 69eda1a384d4e..3fb390b431182 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 @@ -258,5 +258,29 @@ export async function createChatService({ shareReplay() ); }, + async getContextualSuggestions({ + connectorId, + conversationId, + signal, + }: { + connectorId: string; + conversationId: string; + signal: AbortSignal; + }) { + return await client( + 'POST /internal/observability_ai_assistant/conversation/{conversationId}/suggestions', + { + params: { + path: { + conversationId, + }, + body: { + connectorId, + }, + }, + signal, + } + ); + }, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index d1e0855e754a9..3c36e9ff22db3 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -11,7 +11,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { createCallObservabilityAIAssistantAPI } from '../api'; import type { ChatRegistrationRenderFunction, ObservabilityAIAssistantService } from '../types'; -import type { ChatContext } from '../../common/types'; +import type { ChatContext, Suggestion } from '../../common/types'; export function createService({ analytics, @@ -31,6 +31,7 @@ export function createService({ const client = createCallObservabilityAIAssistantAPI(coreStart); const registrations: ChatRegistrationRenderFunction[] = []; + const suggestions: Suggestion[] = []; let chatContext: ChatContext = {}; @@ -41,6 +42,9 @@ export function createService({ register: (fn) => { registrations.push(fn); }, + registerStarterSuggestions(newSuggestions: Suggestion[]) { + suggestions.push(...newSuggestions); + }, start: async ({ signal }) => { const mod = await import('./create_chat_service'); return await mod.createChatService({ analytics, client, signal, registrations }); @@ -49,6 +53,7 @@ export function createService({ getCurrentUser: () => securityStart.authc.getCurrentUser(), getLicense: () => licenseStart.license$, getLicenseManagementLocator: () => shareStart, + getStarterSuggestions: () => suggestions, setChatContext: (newChatContext: ChatContext) => { chatContext = { ...chatContext, diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 2e7bba6060f68..881128def9ad5 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -40,6 +40,7 @@ import type { FunctionResponse, Message, PendingMessage, + Suggestion, } from '../common/types'; import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './components/chat/types'; import type { ObservabilityAIAssistantAPIClient } from './api'; @@ -72,6 +73,11 @@ export interface ObservabilityAIAssistantChatService { }) => Observable; getContexts: () => ContextDefinition[]; getFunctions: (options?: { contexts?: string[]; filter?: string }) => FunctionDefinition[]; + getContextualSuggestions: (options: { + connectorId: string; + conversationId: string; + signal: AbortSignal; + }) => Promise; hasFunction: (name: string) => boolean; hasRenderFunction: (name: string) => boolean; renderFunction: ( @@ -89,8 +95,10 @@ export interface ObservabilityAIAssistantService { getCurrentUser: () => Promise; getLicense: () => Observable; getLicenseManagementLocator: () => SharePluginStart; + getStarterSuggestions: () => Suggestion[]; start: ({}: { signal: AbortSignal }) => Promise; register: (fn: ChatRegistrationRenderFunction) => void; + registerStarterSuggestions: (suggestion: Suggestion[]) => void; setChatContext: (newChatContext: ChatContext) => void; getChatContext: () => ChatContext; } diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index 3315f3f91b3ed..0bd509005713b 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -42,8 +42,6 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ const client = await service.getClient({ request }); - client.setChatContext(params.body.chatContext); - if (!client) { throw notImplemented(); } diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts index b39468c3e06c0..52cb967a18c43 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts @@ -7,7 +7,7 @@ import { notImplemented } from '@hapi/boom'; import * as t from 'io-ts'; import { merge } from 'lodash'; -import { Conversation } from '../../../common/types'; +import { Conversation, Suggestion } from '../../../common/types'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { conversationCreateRt, conversationUpdateRt } from '../runtime_types'; @@ -162,6 +162,52 @@ const deleteConversationRoute = createObservabilityAIAssistantServerRoute({ }, }); +const getSuggestionsForConversationRoute = createObservabilityAIAssistantServerRoute({ + endpoint: 'POST /internal/observability_ai_assistant/conversation/{conversationId}/suggestions', + params: t.type({ + path: t.type({ + conversationId: t.string, + }), + body: t.type({ + connectorId: t.string, + }), + }), + + options: { + tags: ['access:ai_assistant'], + }, + handler: async (resources): Promise => { + const { service, request, params } = resources; + + const client = await service.getClient({ request }); + + if (!client) { + throw notImplemented(); + } + + const { + body: { connectorId }, + path: { conversationId }, + } = params; + + const controller = new AbortController(); + + request.events.aborted$.subscribe(() => { + controller.abort(); + }); + + const suggestions = client.getContextualSuggestions({ + conversationId, + connectorId, + signal: controller.signal, + }); + + return (await suggestions) + .split('\n') + .map((suggestion) => ({ prompt: suggestion.replace('"', '').slice(2) })); + }, +}); + export const conversationRoutes = { ...getConversationRoute, ...findConversationsRoute, @@ -169,4 +215,5 @@ export const conversationRoutes = { ...updateConversationRoute, ...updateConversationTitle, ...deleteConversationRoute, + ...getSuggestionsForConversationRoute, }; diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index 7ecbfa77dddc0..659bc3748e487 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -754,4 +754,36 @@ export class ObservabilityAIAssistantClient { clearChatContext = () => { this.chatContext = {}; }; + + getContextualSuggestions = async ({ + connectorId, + conversationId, + signal, + }: { + connectorId: string; + conversationId: string; + signal: AbortSignal; + }) => { + const conversation = await this.get(conversationId); + + const response$ = await this.chat('generate_suggestions', { + messages: [ + { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.User, + content: conversation.messages.slice(1).reduce((acc, curr) => { + return `${acc} ${curr.message.role}: ${curr.message.content}`; + }, 'You are a helpful assistant for Elastic Observability. Assume the following message is a conversation between you and the user. On the basis of this conversation, suggest four things the user can do now. Phrase the suggestions like the user is asking you a follow up question. Return suggestions as an unordered list. Do not use quotes. Here is the content:'), + }, + }, + ], + connectorId, + signal, + }); + + const response = await lastValueFrom(response$.pipe(concatenateChatCompletionChunks())); + + return response.message?.content || ''; + }; } From e4d60dd642e78fa7afdc764c0c9e70711e66e7b2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 09:15:09 +0100 Subject: [PATCH 04/17] Revert "Add starter and contextual suggestions" This reverts commit 3018007dda008cb82aeb74618403e30375f6ee12. --- x-pack/plugins/apm/public/plugin.ts | 5 -- x-pack/plugins/observability/public/plugin.ts | 5 -- .../common/types.ts | 5 -- .../public/components/chat/chat_body.tsx | 41 +-------- .../components/chat/welcome_message.test.tsx | 90 ++++--------------- .../components/chat/welcome_message.tsx | 19 +--- .../suggestions/suggestion_button.tsx | 27 ------ .../components/suggestions/suggestions.tsx | 54 ----------- .../components/technical_preview_badge.tsx | 27 ++++++ .../hooks/use_contextual_suggestions.ts | 76 ---------------- .../public/service/create_chat_service.ts | 24 ----- .../public/service/create_service.ts | 7 +- .../public/types.ts | 8 -- .../server/routes/chat/route.ts | 2 + .../server/routes/conversations/route.ts | 49 +--------- .../server/service/client/index.ts | 32 ------- 16 files changed, 51 insertions(+), 420 deletions(-) delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx delete mode 100644 x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx create mode 100644 x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx delete mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 7148a8ed1f2b5..476a4e1fd5b82 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -455,11 +455,6 @@ export class ApmPlugin implements Plugin { } ); - plugins.observabilityAIAssistant.service.registerStarterSuggestions([ - { app: 'apm', prompt: 'How are my services doing?' }, - { app: 'apm', prompt: 'Do my services have errors?' }, - ]); - if (fleet) { const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData(); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 45dfee7b0ff70..5d6e825d83c43 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -458,11 +458,6 @@ export class Plugin ); }); - pluginsStart.observabilityAIAssistant.service.registerStarterSuggestions([ - { app: 'observability', prompt: 'How are my alerts doing?' }, - { app: 'observability', prompt: 'Can you help me set up an SLO?' }, - ]); - pluginsStart.observabilityShared.updateGlobalNavigation({ capabilities: application.capabilities, deepLinks: this.deepLinks, diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 0cdf0c27dea9f..296f08ae0552f 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -133,8 +133,3 @@ export interface ChatContext { description: string; }; } - -export interface Suggestion { - app?: string; - prompt: string; -} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx index 5de4652e23dad..8ed26d71acc58 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.tsx @@ -28,7 +28,7 @@ import { useLicense } from '../../hooks/use_license'; import { useObservabilityAIAssistantChatService } from '../../hooks/use_observability_ai_assistant_chat_service'; import type { UseGenAIConnectorsResult } from '../../hooks/use_genai_connectors'; import type { UseKnowledgeBaseResult } from '../../hooks/use_knowledge_base'; -import { type Conversation, type Message, MessageRole, Suggestion } from '../../../common/types'; +import { type Conversation, type Message, MessageRole } from '../../../common/types'; import { ChatHeader } from './chat_header'; import { PromptEditor } from '../prompt_editor/prompt_editor'; import { ChatTimeline } from './chat_timeline'; @@ -43,8 +43,6 @@ import { import { ASSISTANT_SETUP_TITLE, EMPTY_CONVERSATION_TITLE, UPGRADE_LICENSE_TITLE } from '../../i18n'; import type { StartedFrom } from '../../utils/get_timeline_items_from_conversation'; import { TELEMETRY, sendEvent } from '../../analytics'; -import { Suggestions } from '../suggestions/suggestions'; -import { useContextualSuggestions } from '../../hooks/use_contextual_suggestions'; import { FlyoutWidthMode } from './chat_flyout'; const fullHeightClassName = css` @@ -127,8 +125,6 @@ export function ChatBody({ const euiTheme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(euiTheme); - const { signal } = new AbortController(); - const chatService = useObservabilityAIAssistantChatService(); const { conversation, messages, next, state, stop, saveTitle } = useConversation({ @@ -170,15 +166,6 @@ export function ChatBody({ : '100%'}; `; - const suggestions = useContextualSuggestions({ - chatService, - connectors, - conversation, - messages, - signal, - state, - }); - const [stickToBottom, setStickToBottom] = useState(true); const isAtBottom = (parent: HTMLElement) => @@ -307,17 +294,6 @@ export function ChatBody({ } }; - const handleSuggestionClick = (suggestion: Suggestion) => { - next( - messages.concat([ - { - '@timestamp': new Date().toISOString(), - message: { role: MessageRole.User, content: suggestion.prompt }, - }, - ]) - ); - }; - if (!hasCorrectLicense && !initialConversationId) { footer = ( <> @@ -361,11 +337,7 @@ export function ChatBody({ className={animClassName} > {connectors.connectors?.length === 0 || messages.length === 1 ? ( - + ) : ( )} - {conversation && conversation.value && 'id' in conversation.value.conversation ? ( - - - - ) : null} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx index b8e8c5cf5a0f6..56dd52c417a3b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.test.tsx @@ -119,11 +119,7 @@ describe('Welcome Message', () => { describe('when no connectors are available', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - {}} - /> + ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -131,11 +127,7 @@ describe('Welcome Message', () => { it('should show a set up connector button', () => { const { getByTestId } = render( - {}} - /> + ); expect( @@ -146,11 +138,7 @@ describe('Welcome Message', () => { describe('when no triggersactionsUi capabilities are available', () => { it('should navigate to stack management', () => { const { getByTestId } = render( - {}} - /> + ); fireEvent.click( @@ -192,11 +180,7 @@ describe('Welcome Message', () => { it('should render a connector flyout when clicking the set up connector button', () => { const { getByTestId } = render( - {}} - /> + ); fireEvent.click( @@ -235,11 +219,7 @@ describe('Welcome Message', () => { describe('when connectors are available', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - {}} - /> + ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -248,11 +228,7 @@ describe('Welcome Message', () => { describe('when knowledge base is not installed', () => { it('should render the retry and inspect errors buttons', () => { const { getByTestId } = render( - {}} - /> + ); expect( @@ -266,11 +242,7 @@ describe('Welcome Message', () => { it('should call kb install when clicking retry', async () => { const { getByTestId } = render( - {}} - /> + ); await act(async () => { @@ -283,11 +255,7 @@ describe('Welcome Message', () => { it('should render a popover with installation errors when clicking inspect', async () => { const { getByTestId } = render( - {}} - /> + ); fireEvent.click(getByTestId('observabilityAiAssistantWelcomeMessageInspectErrorsButton')); @@ -311,11 +279,7 @@ describe('Welcome Message', () => { it('should navigate to ML when clicking the link in the error popover', async () => { const { getByTestId } = render( - {}} - /> + ); fireEvent.click(getByTestId('observabilityAiAssistantWelcomeMessageInspectErrorsButton')); @@ -332,11 +296,7 @@ describe('Welcome Message', () => { describe('when knowledge base is installing', () => { it('should not show a failure message', () => { const { queryByTestId } = render( - {}} - /> + ); expect( @@ -350,11 +310,7 @@ describe('Welcome Message', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - {}} - /> + ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -364,11 +320,7 @@ describe('Welcome Message', () => { describe('when knowledge base is loading', () => { it('should not show a failure message', () => { const { queryByTestId } = render( - {}} - /> + ); expect( @@ -382,11 +334,7 @@ describe('Welcome Message', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - {}} - /> + ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); @@ -396,11 +344,7 @@ describe('Welcome Message', () => { describe('when knowledge base is installed', () => { it('should not show a failure message', () => { const { queryByTestId } = render( - {}} - /> + ); expect( @@ -414,11 +358,7 @@ describe('Welcome Message', () => { it('should show a disclaimer', () => { const { getByTestId } = render( - {}} - /> + ); expect(getByTestId('observabilityAiAssistantDisclaimer')).toBeInTheDocument(); diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx index 8c63a07b2c78b..bf514691f7d93 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/welcome_message.tsx @@ -25,8 +25,6 @@ import { Disclaimer } from './disclaimer'; import { WelcomeMessageConnectors } from './welcome_message_connectors'; import { WelcomeMessageKnowledgeBase } from './welcome_message_knowledge_base'; import { useKibana } from '../../hooks/use_kibana'; -import { Suggestions } from '../suggestions/suggestions'; -import { useObservabilityAIAssistant } from '../../hooks/use_observability_ai_assistant'; const fullHeightClassName = css` height: 100%; @@ -40,15 +38,10 @@ const centerMaxWidthClassName = css` export function WelcomeMessage({ connectors, knowledgeBase, - onSelectSuggestion, }: { connectors: UseGenAIConnectorsResult; knowledgeBase: UseKnowledgeBaseResult; - onSelectSuggestion: ({ prompt }: { prompt: string }) => void; }) { - const service = useObservabilityAIAssistant(); - - const suggestions = service.getStarterSuggestions(); const breakpoint = useCurrentEuiBreakpoint(); const { @@ -97,13 +90,13 @@ export function WelcomeMessage({ - +

{i18n.translate('xpack.observabilityAiAssistant.disclaimer.title', { defaultMessage: 'Welcome to the AI Assistant for Observability', @@ -119,12 +112,6 @@ export function WelcomeMessage({ /> - - - - - - diff --git a/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx b/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx deleted file mode 100644 index 94385c7bfab32..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestion_button.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import type { Suggestion } from '../../../common/types'; - -export function SuggestionButton({ - suggestion, - onSelect, -}: { - suggestion: Suggestion; - onSelect: ({ prompt }: { prompt: string }) => void; -}) { - const handleClick = () => onSelect({ prompt: suggestion.prompt }); - - return ( - - - {suggestion.prompt} - - - ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx b/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx deleted file mode 100644 index 5b101242c66ce..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/components/suggestions/suggestions.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/css'; -import { SuggestionButton } from './suggestion_button'; -import type { Suggestion } from '../../../common/types'; - -const GUTTER_SIZE = 's'; - -export function Suggestions({ - isLoading, - suggestions = [], - onSelect, -}: { - isLoading: boolean; - suggestions: Suggestion[]; - onSelect: ({ prompt }: { prompt: string }) => void; -}) { - const { euiTheme } = useEuiTheme(); - - const buttonContainerClassName = css` - min-width: calc(50% - ${euiTheme.size[GUTTER_SIZE]}); - max-width: calc(50% - ${euiTheme.size[GUTTER_SIZE]}); - `; - - const appName = window.location.pathname - .split('/') - .reduce((acc, pathPartial, index, arr) => (pathPartial === 'app' ? arr[index + 1] : acc), ''); - - const filteredSuggestions = useMemo(() => { - return suggestions.sort((a) => (a.app === appName ? -1 : 1)).slice(0, 4) || []; - }, [appName, suggestions]); - - return ( - - {isLoading ? ( - - - - ) : ( - filteredSuggestions.map((suggestion) => ( - - - - )) - )} - - ); -} diff --git a/x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx b/x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx new file mode 100644 index 0000000000000..e1ddd86c2017c --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/public/components/technical_preview_badge.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBetaBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function TechnicalPreviewBadge() { + return ( + + ); +} diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts deleted file mode 100644 index b492276ec16e8..0000000000000 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_contextual_suggestions.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState } from 'react'; -import { ConversationRequestBase, Message } from '../../common/types'; -import { AbortableAsyncState, useAbortableAsync } from './use_abortable_async'; -import { ChatState } from './use_chat'; -import type { UseGenAIConnectorsResult } from './use_genai_connectors'; -import type { Conversation } from '../../common'; - -export function useContextualSuggestions({ - chatService, - connectors, - conversation, - messages, - state, - signal, -}: { - chatService: any; - connectors: UseGenAIConnectorsResult; - conversation: AbortableAsyncState; - messages: Message[]; - state: ChatState; - signal: AbortSignal | null; -}) { - const [lastMessageContent, setLastMessageContent] = useState( - messages[messages.length - 1]?.message.content - ); - - const [conversationId, setConversationId] = useState(''); - - const suggestions = useAbortableAsync(() => { - return connectors.selectedConnector && - conversation.value?.conversation && - 'id' in conversation.value.conversation - ? chatService.getContextualSuggestions({ - connectorId: connectors.selectedConnector, - conversationId: conversation.value.conversation.id, - signal, - }) - : Promise.resolve([]); - }, []); - - useEffect(() => { - if (state !== ChatState.Loading) { - if (lastMessageContent !== messages[messages.length - 1]?.message.content) { - setLastMessageContent(messages[messages.length - 1]?.message.content); - suggestions.refresh(); - } - } - if ( - conversation.value?.conversation && - 'id' in conversation.value.conversation && - conversationId !== conversation.value.conversation.id - ) { - setConversationId(conversation.value?.conversation.id); - suggestions.refresh(); - } - }, [ - conversation.value?.conversation, - conversationId, - lastMessageContent, - messages, - state, - suggestions, - ]); - - return { - value: suggestions.value, - loading: suggestions.loading, - }; -} 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 3fb390b431182..69eda1a384d4e 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 @@ -258,29 +258,5 @@ export async function createChatService({ shareReplay() ); }, - async getContextualSuggestions({ - connectorId, - conversationId, - signal, - }: { - connectorId: string; - conversationId: string; - signal: AbortSignal; - }) { - return await client( - 'POST /internal/observability_ai_assistant/conversation/{conversationId}/suggestions', - { - params: { - path: { - conversationId, - }, - body: { - connectorId, - }, - }, - signal, - } - ); - }, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index 3c36e9ff22db3..d1e0855e754a9 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -11,7 +11,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { createCallObservabilityAIAssistantAPI } from '../api'; import type { ChatRegistrationRenderFunction, ObservabilityAIAssistantService } from '../types'; -import type { ChatContext, Suggestion } from '../../common/types'; +import type { ChatContext } from '../../common/types'; export function createService({ analytics, @@ -31,7 +31,6 @@ export function createService({ const client = createCallObservabilityAIAssistantAPI(coreStart); const registrations: ChatRegistrationRenderFunction[] = []; - const suggestions: Suggestion[] = []; let chatContext: ChatContext = {}; @@ -42,9 +41,6 @@ export function createService({ register: (fn) => { registrations.push(fn); }, - registerStarterSuggestions(newSuggestions: Suggestion[]) { - suggestions.push(...newSuggestions); - }, start: async ({ signal }) => { const mod = await import('./create_chat_service'); return await mod.createChatService({ analytics, client, signal, registrations }); @@ -53,7 +49,6 @@ export function createService({ getCurrentUser: () => securityStart.authc.getCurrentUser(), getLicense: () => licenseStart.license$, getLicenseManagementLocator: () => shareStart, - getStarterSuggestions: () => suggestions, setChatContext: (newChatContext: ChatContext) => { chatContext = { ...chatContext, diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 881128def9ad5..2e7bba6060f68 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -40,7 +40,6 @@ import type { FunctionResponse, Message, PendingMessage, - Suggestion, } from '../common/types'; import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './components/chat/types'; import type { ObservabilityAIAssistantAPIClient } from './api'; @@ -73,11 +72,6 @@ export interface ObservabilityAIAssistantChatService { }) => Observable; getContexts: () => ContextDefinition[]; getFunctions: (options?: { contexts?: string[]; filter?: string }) => FunctionDefinition[]; - getContextualSuggestions: (options: { - connectorId: string; - conversationId: string; - signal: AbortSignal; - }) => Promise; hasFunction: (name: string) => boolean; hasRenderFunction: (name: string) => boolean; renderFunction: ( @@ -95,10 +89,8 @@ export interface ObservabilityAIAssistantService { getCurrentUser: () => Promise; getLicense: () => Observable; getLicenseManagementLocator: () => SharePluginStart; - getStarterSuggestions: () => Suggestion[]; start: ({}: { signal: AbortSignal }) => Promise; register: (fn: ChatRegistrationRenderFunction) => void; - registerStarterSuggestions: (suggestion: Suggestion[]) => void; setChatContext: (newChatContext: ChatContext) => void; getChatContext: () => ChatContext; } diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index 0bd509005713b..3315f3f91b3ed 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -42,6 +42,8 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ const client = await service.getClient({ request }); + client.setChatContext(params.body.chatContext); + if (!client) { throw notImplemented(); } diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts index 52cb967a18c43..b39468c3e06c0 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/conversations/route.ts @@ -7,7 +7,7 @@ import { notImplemented } from '@hapi/boom'; import * as t from 'io-ts'; import { merge } from 'lodash'; -import { Conversation, Suggestion } from '../../../common/types'; +import { Conversation } from '../../../common/types'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; import { conversationCreateRt, conversationUpdateRt } from '../runtime_types'; @@ -162,52 +162,6 @@ const deleteConversationRoute = createObservabilityAIAssistantServerRoute({ }, }); -const getSuggestionsForConversationRoute = createObservabilityAIAssistantServerRoute({ - endpoint: 'POST /internal/observability_ai_assistant/conversation/{conversationId}/suggestions', - params: t.type({ - path: t.type({ - conversationId: t.string, - }), - body: t.type({ - connectorId: t.string, - }), - }), - - options: { - tags: ['access:ai_assistant'], - }, - handler: async (resources): Promise => { - const { service, request, params } = resources; - - const client = await service.getClient({ request }); - - if (!client) { - throw notImplemented(); - } - - const { - body: { connectorId }, - path: { conversationId }, - } = params; - - const controller = new AbortController(); - - request.events.aborted$.subscribe(() => { - controller.abort(); - }); - - const suggestions = client.getContextualSuggestions({ - conversationId, - connectorId, - signal: controller.signal, - }); - - return (await suggestions) - .split('\n') - .map((suggestion) => ({ prompt: suggestion.replace('"', '').slice(2) })); - }, -}); - export const conversationRoutes = { ...getConversationRoute, ...findConversationsRoute, @@ -215,5 +169,4 @@ export const conversationRoutes = { ...updateConversationRoute, ...updateConversationTitle, ...deleteConversationRoute, - ...getSuggestionsForConversationRoute, }; diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index 6621504cf3e86..e7a88343d16b4 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -790,36 +790,4 @@ export class ObservabilityAIAssistantClient { clearChatContext = () => { this.chatContext = {}; }; - - getContextualSuggestions = async ({ - connectorId, - conversationId, - signal, - }: { - connectorId: string; - conversationId: string; - signal: AbortSignal; - }) => { - const conversation = await this.get(conversationId); - - const response$ = await this.chat('generate_suggestions', { - messages: [ - { - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.User, - content: conversation.messages.slice(1).reduce((acc, curr) => { - return `${acc} ${curr.message.role}: ${curr.message.content}`; - }, 'You are a helpful assistant for Elastic Observability. Assume the following message is a conversation between you and the user. On the basis of this conversation, suggest four things the user can do now. Phrase the suggestions like the user is asking you a follow up question. Return suggestions as an unordered list. Do not use quotes. Here is the content:'), - }, - }, - ], - connectorId, - signal, - }); - - const response = await lastValueFrom(response$.pipe(concatenateChatCompletionChunks())); - - return response.message?.content || ''; - }; } From f4a1fb915fa9311a65e0f134ca5c6d037ca0ceb2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 12:29:48 +0100 Subject: [PATCH 05/17] Support for data in appContext --- .../app/service_inventory/index.tsx | 31 +++++- .../app/transaction_overview/index.tsx | 13 ++- .../routing/templates/apm_main_template.tsx | 27 +++++- .../shared/transactions_table/index.tsx | 24 ++++- .../common/types.ts | 10 +- .../action_menu_item/action_menu_item.tsx | 17 ++-- .../public/hooks/use_chat.ts | 2 +- .../public/hooks/use_conversation.test.tsx | 2 + ...observability_ai_assistant_chat_context.ts | 15 --- .../public/mock.tsx | 9 +- .../public/plugin.tsx | 2 - .../public/service/create_chat_service.ts | 4 +- .../public/service/create_service.ts | 17 ++-- .../public/types.ts | 10 +- .../server/functions/context.ts | 10 +- .../server/functions/index.ts | 3 + .../server/routes/chat/route.ts | 11 +-- .../server/routes/functions/route.ts | 1 + .../server/routes/runtime_types.ts | 29 +++--- .../chat_function_client/index.test.ts | 97 ++++++++++++++++++- .../service/chat_function_client/index.ts | 79 +++++++++++++-- .../server/service/client/index.test.ts | 2 +- .../server/service/client/index.ts | 43 +++----- .../server/service/index.ts | 38 +++----- .../server/service/types.ts | 2 + 25 files changed, 351 insertions(+), 147 deletions(-) delete mode 100644 x-pack/plugins/observability_ai_assistant/public/hooks/use_observability_ai_assistant_chat_context.ts diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 55bde1c8fcf2b..33a5760d0eb22 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -7,7 +7,7 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { APIReturnType } from '../../../services/rest/create_call_apm_api'; import { useStateDebounced } from '../../../hooks/use_debounce'; @@ -29,6 +29,7 @@ import { isTimeComparison } from '../../shared/time_comparison/get_comparison_op import { ServiceList } from './service_list'; import { orderServiceItems } from './service_list/order_service_items'; import { SortFunction } from '../../shared/managed_table'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/services'>; @@ -265,6 +266,34 @@ export function ServiceInventory() { [tiebreakerField] ); + const setApplicationContext = + useApmPluginContext().observabilityAIAssistant.service + .setApplicationContext; + + useEffect(() => { + if (mainStatisticsStatus === FETCH_STATUS.FAILURE) { + return setApplicationContext({ + description: 'The services have failed to load', + }); + } + + if (mainStatisticsStatus === FETCH_STATUS.LOADING) { + return setApplicationContext({ + description: 'The services are still loading', + }); + } + + return setApplicationContext({ + data: [ + { + name: 'services', + description: 'The list of services that the user is looking at', + value: mainStatisticsData.items, + }, + ], + }); + }, [mainStatisticsStatus, mainStatisticsData.items]); + return ( <> diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 73f5e40aa2e2a..6859957092906 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -6,9 +6,10 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; import { isServerlessAgentName } from '../../../../common/agent_name'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useLocalStorage } from '../../../hooks/use_local_storage'; @@ -55,6 +56,16 @@ export function TransactionOverview() { false ); + const setApplicationContext = + useApmPluginContext().observabilityAIAssistant.service + .setApplicationContext; + + useEffect(() => { + return setApplicationContext({ + description: `The user is looking at the transactions overview for ${serviceName}, and the transaction type is ${transactionType}`, + }); + }, [setApplicationContext]); + return ( <> {!sloCalloutDismissed && ( diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index 5e2e0964be791..8f5963eb55bea 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPageHeaderProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; -import React, { useContext } from 'react'; +import React, { useContext, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; import { KibanaEnvironmentContext } from '../../../context/kibana_environment_context/kibana_environment_context'; @@ -66,6 +66,8 @@ export function ApmMainTemplate({ const basePath = http?.basePath.get(); const { config } = useApmPluginContext(); + const aiAssistant = services.observabilityAIAssistant.service; + const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; const { data, status } = useFetcher((callApmApi) => { @@ -103,16 +105,35 @@ export function ApmMainTemplate({ status === FETCH_STATUS.LOADING || fleetApmPoliciesStatus === FETCH_STATUS.LOADING; + const hasApmData = !!data?.hasData; + const hasApmIntegrations = !!fleetApmPoliciesData?.hasApmPolicies; + const noDataConfig = getNoDataConfig({ basePath, docsLink: docLinks!.links.observability.guide, - hasApmData: data?.hasData, - hasApmIntegrations: fleetApmPoliciesData?.hasApmPolicies, + hasApmData, + hasApmIntegrations, shouldBypassNoDataScreen, loading: isLoading, isServerless: config?.serverlessOnboarding, }); + useEffect(() => { + return aiAssistant.setApplicationContext({ + description: [ + hasApmData + ? 'The user has APM data.' + : 'The user does not have APM data.', + hasApmIntegrations + ? 'The user has the APM integration installed. ' + : 'The user does not have the APM integration installed', + !noDataConfig + ? '' + : 'The user is looking at a screen that tells them they do not have any data.', + ].join('\n'), + }); + }, [hasApmData, hasApmIntegrations, !!noDataConfig]); + const rightSideItems = [ ...(showServiceGroupSaveButton ? [] : []), ]; diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 0d9621efd18c8..93e0e60e1c404 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { v4 as uuidv4 } from 'uuid'; import { FormattedMessage } from '@kbn/i18n-react'; import { compact } from 'lodash'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { ApmDocumentType } from '../../../../common/document_type'; import { getLatencyAggregationType, @@ -33,6 +33,7 @@ import { ManagedTable, TableSearchBar } from '../managed_table'; import { OverviewTableContainer } from '../overview_table_container'; import { isTimeComparison } from '../time_comparison/get_comparison_options'; import { getColumns } from './get_columns'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; type ApiResponse = APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics'>; @@ -163,6 +164,27 @@ export function TransactionsTable({ }; }, [mainStatistics.maxCountExceeded, setSearchQueryDebounced]); + const setApplicationContext = + useApmPluginContext().observabilityAIAssistant.service + .setApplicationContext; + + useEffect(() => { + return setApplicationContext({ + data: [ + { + name: 'top_transactions', + description: 'The visible transaction groups', + value: mainStatistics.transactionGroups.map((group) => { + return { + name: group.name, + alertsCount: group.alertsCount, + }; + }), + }, + ], + }); + }, [setApplicationContext, mainStatistics]); + return ( void; export type ContextRegistry = Map; export type FunctionRegistry = Map; -export interface ChatContext { - [key: string]: { - value: string | number; +export interface ObservabilityAIAssistantAppContext { + description?: string; + data?: Array<{ + name: string; description: string; - }; + value: any; + }>; } diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index 2edd31bfe4d19..3e3aa7d4e387b 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -44,17 +44,20 @@ export function ObservabilityAIAssistantActionMenuItem() { }; }, []); + useEffect(() => { + const unregister = service.setApplicationContext({ + description: 'The user is looking at ' + window.location.href, + }); + + return () => { + unregister(); + }; + }, []); + if (!service.isEnabled()) { return null; } - service.setChatContext({ - url: { - value: window.location.href, - description: 'The URL that the user is currently looking at', - }, - }); - return ( <> {}, } as unknown as SharePluginStart), register: () => {}, - setChatContext: () => {}, - getChatContext: () => ({}), + setApplicationContext: () => noop, + getApplicationContexts: () => [], }; function createSetupContract(): ObservabilityAIAssistantPluginSetup { @@ -89,10 +90,6 @@ function createStartContract(): ObservabilityAIAssistantPluginStart { selectConnector: () => {}, reloadConnectors: () => {}, }), - useObservabilityAIAssistantChatContext: () => ({ - setChatContext: () => {}, - getChatContext: () => ({}), - }), }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx index 888da2b5dab4a..5ee13a1c5b6e8 100644 --- a/x-pack/plugins/observability_ai_assistant/public/plugin.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/plugin.tsx @@ -21,7 +21,6 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { withSuspense } from '@kbn/shared-ux-utility'; import { createService } from './service/create_service'; import { useGenAIConnectorsWithoutContext } from './hooks/use_genai_connectors'; -import { useObservabilityAIAssistantChatContext } from './hooks/use_observability_ai_assistant_chat_context'; import type { ConfigSchema, ObservabilityAIAssistantPluginSetup, @@ -147,7 +146,6 @@ export class ObservabilityAIAssistantPlugin return { service, useGenAIConnectors: () => useGenAIConnectorsWithoutContext(service), - useObservabilityAIAssistantChatContext: () => useObservabilityAIAssistantChatContext(service), ObservabilityAIAssistantContextualInsight: isEnabled ? withSuspense( withProviders( 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 69eda1a384d4e..f160c864e7ba5 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 @@ -145,14 +145,14 @@ export async function createChatService({ hasRenderFunction: (name: string) => { return renderFunctionRegistry.has(name); }, - complete({ chatContext, connectorId, conversationId, messages, persist, signal }) { + complete({ appContexts, connectorId, conversationId, messages, persist, signal }) { return new Observable((subscriber) => { client('POST /internal/observability_ai_assistant/chat/complete', { params: { body: { connectorId, conversationId, - chatContext, + appContexts, messages, persist, }, diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index d1e0855e754a9..62e60b15925a1 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -9,9 +9,10 @@ import type { AnalyticsServiceStart, CoreStart } from '@kbn/core/public'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { remove } from 'lodash'; +import { ObservabilityAIAssistantAppContext } from '../../common/types'; import { createCallObservabilityAIAssistantAPI } from '../api'; import type { ChatRegistrationRenderFunction, ObservabilityAIAssistantService } from '../types'; -import type { ChatContext } from '../../common/types'; export function createService({ analytics, @@ -32,7 +33,7 @@ export function createService({ const registrations: ChatRegistrationRenderFunction[] = []; - let chatContext: ChatContext = {}; + let appContexts: Array = []; return { isEnabled: () => { @@ -49,12 +50,14 @@ export function createService({ getCurrentUser: () => securityStart.authc.getCurrentUser(), getLicense: () => licenseStart.license$, getLicenseManagementLocator: () => shareStart, - setChatContext: (newChatContext: ChatContext) => { - chatContext = { - ...chatContext, - ...newChatContext, + setApplicationContext: (context: ObservabilityAIAssistantAppContext) => { + appContexts.push(context); + return () => { + remove(appContexts, context); }; }, - getChatContext: () => chatContext, + getApplicationContexts: () => { + return appContexts; + }, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 2e7bba6060f68..8c0220271d4a0 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -34,11 +34,11 @@ import type { } from '@kbn/triggers-actions-ui-plugin/public'; import type { StreamingChatResponseEventWithoutError } from '../common/conversation_complete'; import type { - ChatContext, ContextDefinition, FunctionDefinition, FunctionResponse, Message, + ObservabilityAIAssistantAppContext, PendingMessage, } from '../common/types'; import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './components/chat/types'; @@ -50,7 +50,6 @@ import type { UseGenAIConnectorsResult } from './hooks/use_genai_connectors'; export type { CreateChatCompletionResponseChunk } from '../common/types'; export type { PendingMessage }; - export interface ObservabilityAIAssistantChatService { analytics: AnalyticsServiceStart; chat: ( @@ -63,7 +62,7 @@ export interface ObservabilityAIAssistantChatService { } ) => Observable; complete: (options: { - chatContext: ChatContext; + appContexts: ObservabilityAIAssistantAppContext[]; conversationId?: string; connectorId: string; messages: Message[]; @@ -91,8 +90,8 @@ export interface ObservabilityAIAssistantService { getLicenseManagementLocator: () => SharePluginStart; start: ({}: { signal: AbortSignal }) => Promise; register: (fn: ChatRegistrationRenderFunction) => void; - setChatContext: (newChatContext: ChatContext) => void; - getChatContext: () => ChatContext; + setApplicationContext: (appContext: ObservabilityAIAssistantAppContext) => () => void; + getApplicationContexts: () => ObservabilityAIAssistantAppContext[]; } export type RenderFunction = (options: { @@ -146,5 +145,4 @@ export interface ObservabilityAIAssistantPluginStart { RefAttributes<{}> > | null; useGenAIConnectors: () => UseGenAIConnectorsResult; - useObservabilityAIAssistantChatContext: () => {}; } diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts index 5ff72333c0d05..5365c1e59426f 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts @@ -58,15 +58,13 @@ export function registerContextFunction({ required: ['queries', 'categories'], } as const, }, - async ({ arguments: { queries, categories }, messages, connectorId }, signal) => { + async ({ arguments: { queries, categories }, messages, connectorId, appContexts }, signal) => { const systemMessage = messages.find((message) => message.message.role === MessageRole.System); if (!systemMessage) { throw new Error('No system message found'); } - const chatContext = await client.getChatContext(); - const userMessage = last( messages.filter((message) => message.message.role === MessageRole.User) ); @@ -84,9 +82,11 @@ export function registerContextFunction({ queries: queriesOrUserPrompt, }); + const screenDescription = appContexts.map((context) => context.description).join('\n\n'); + if (suggestions.length === 0) { return { - content: { learnings: [] as unknown as Serializable, chatContext }, + content: { learnings: [] as unknown as Serializable, screenDescription }, }; } @@ -101,7 +101,7 @@ export function registerContextFunction({ }); return { - content: { learnings: relevantDocuments as unknown as Serializable, chatContext }, + content: { learnings: relevantDocuments as unknown as Serializable, screenDescription }, }; } ); diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts index 7a65535318f6d..fdcf95b7bf671 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts @@ -67,6 +67,9 @@ export const registerFunctions: ChatRegistrationFunction = async ({ If the "execute_query" function has been called, summarize these results for the user. The user does not see a visualization in this case. If the "get_dataset_info" function returns no data, and the user asks for a query, generate a query anyway with the "query" function, but be explicit about it potentially being incorrect. + + You have access to data on the screen by calling the "get_data_on_screen" function if it is available. Use it to help the user understand what they are looking at. + A short summary of what they are looking at is available in the return of the "context" function. ` ); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index 3315f3f91b3ed..f417d199136e9 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -10,7 +10,7 @@ import { toBooleanRt } from '@kbn/io-ts-utils'; import type OpenAI from 'openai'; import { Readable } from 'stream'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; -import { messageRt, chatContextRt } from '../runtime_types'; +import { messageRt, appContextRt } from '../runtime_types'; import { observableIntoStream } from '../../service/util/observable_into_stream'; const chatRoute = createObservabilityAIAssistantServerRoute({ @@ -42,8 +42,6 @@ const chatRoute = createObservabilityAIAssistantServerRoute({ const client = await service.getClient({ request }); - client.setChatContext(params.body.chatContext); - if (!client) { throw notImplemented(); } @@ -83,7 +81,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ body: t.intersection([ t.type({ messages: t.array(messageRt), - chatContext: chatContextRt, + appContexts: t.array(appContextRt), connectorId: t.string, persist: toBooleanRt, }), @@ -103,7 +101,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ } const { - body: { messages, connectorId, conversationId, title, persist }, + body: { messages, connectorId, conversationId, title, persist, appContexts }, } = params; const controller = new AbortController(); @@ -116,10 +114,9 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ signal: controller.signal, resources, client, + appContexts, }); - client.setChatContext(params.body.chatContext); - const response$ = client.complete({ messages, connectorId, diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts index bf64f77d45ca3..6409498081f5d 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts @@ -39,6 +39,7 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({ signal: controller.signal, resources, client, + appContexts: [], }); return { diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts index 0ceaca4514883..1f94af80611c4 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts @@ -7,13 +7,13 @@ import * as t from 'io-ts'; import { toBooleanRt } from '@kbn/io-ts-utils'; import { - Conversation, - ConversationCreateRequest, - ConversationRequestBase, - ConversationUpdateRequest, - Message, + type Conversation, + type ConversationCreateRequest, + type ConversationRequestBase, + type ConversationUpdateRequest, + type Message, MessageRole, - ChatContext, + type ObservabilityAIAssistantAppContext, } from '../../common/types'; const serializeableRt = t.any; @@ -94,10 +94,13 @@ export const conversationRt: t.Type = t.intersection([ }), ]); -export const chatContextRt: t.Type = t.record( - t.string, - t.type({ - value: t.union([t.string, t.number]), - description: t.string, - }) -); +export const appContextRt: t.Type = t.partial({ + description: t.string, + data: t.array( + t.type({ + name: t.string, + description: t.string, + value: t.any, + }) + ), +}); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts index 7d34404457d24..37030ffa1c684 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts @@ -5,8 +5,9 @@ * 2.0. */ import Ajv, { type ValidateFunction } from 'ajv'; +import dedent from 'dedent'; import { ChatFunctionClient } from '.'; -import type { ContextRegistry } from '../../../common/types'; +import { ContextRegistry, FunctionVisibility } from '../../../common/types'; import type { FunctionHandlerRegistry } from '../types'; describe('chatFunctionClient', () => { @@ -53,7 +54,28 @@ describe('chatFunctionClient', () => { ) ); - client = new ChatFunctionClient(contextRegistry, functionRegistry, validators); + client = new ChatFunctionClient([]); + client.registerContext({ + description: '', + name: 'core', + }); + + client.registerFunction( + { + contexts: ['core'], + description: '', + name: 'myFunction', + parameters: { + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + }, + respondFn + ); }); it('throws an error', async () => { @@ -72,4 +94,75 @@ describe('chatFunctionClient', () => { expect(respondFn).not.toHaveBeenCalled(); }); }); + + describe('when providing application context', () => { + it('exposes a function that returns the requested data', async () => { + const client = new ChatFunctionClient([ + { + description: 'My description', + data: [ + { + name: 'my_dummy_data', + description: 'My dummy data', + value: [ + { + foo: 'bar', + }, + ], + }, + { + name: 'my_other_dummy_data', + description: 'My other dummy data', + value: [ + { + foo: 'bar', + }, + ], + }, + ], + }, + ]); + + const functions = client.getFunctions(); + + expect(functions[0]).toEqual({ + definition: { + contexts: ['core'], + description: expect.any(String), + name: 'get_data_on_screen', + parameters: expect.any(Object), + visibility: FunctionVisibility.AssistantOnly, + }, + respond: expect.any(Function), + }); + + expect(functions[0].definition.description).toContain( + dedent(`my_dummy_data: My dummy data + my_other_dummy_data: My other dummy data + `) + ); + + const result = await client.executeFunction({ + name: 'get_data_on_screen', + args: JSON.stringify({ data: ['my_dummy_data'] }), + messages: [], + connectorId: '', + signal: new AbortController().signal, + }); + + expect(result).toEqual({ + content: [ + { + name: 'my_dummy_data', + description: 'My dummy data', + value: [ + { + foo: 'bar', + }, + ], + }, + ], + }); + }); + }); }); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts index 202df11f8faa4..a86726174a701 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts @@ -6,16 +6,20 @@ */ /* eslint-disable max-classes-per-file*/ -import type { ErrorObject, ValidateFunction } from 'ajv'; -import { keyBy } from 'lodash'; -import type { +import Ajv, { type ErrorObject, type ValidateFunction } from 'ajv'; +import dedent from 'dedent'; +import { compact, keyBy } from 'lodash'; +import { ContextDefinition, ContextRegistry, FunctionResponse, + FunctionVisibility, Message, + ObservabilityAIAssistantAppContext, + RegisterContextDefinition, } from '../../../common/types'; import { filterFunctionDefinitions } from '../../../common/utils/filter_function_definitions'; -import type { FunctionHandler, FunctionHandlerRegistry } from '../types'; +import type { FunctionHandler, FunctionHandlerRegistry, RegisterFunction } from '../types'; export class FunctionArgsValidationError extends Error { constructor(public readonly errors: ErrorObject[]) { @@ -23,12 +27,64 @@ export class FunctionArgsValidationError extends Error { } } +const ajv = new Ajv({ + strict: false, +}); + export class ChatFunctionClient { - constructor( - private readonly contextRegistry: ContextRegistry, - private readonly functionRegistry: FunctionHandlerRegistry, - private readonly validators: Map - ) {} + private readonly contextRegistry: ContextRegistry = new Map(); + private readonly functionRegistry: FunctionHandlerRegistry = new Map(); + private readonly validators: Map = new Map(); + + constructor(private readonly appContexts: ObservabilityAIAssistantAppContext[]) { + const allData = compact(appContexts.flatMap((context) => context.data)); + + if (allData.length) { + this.registerFunction( + { + name: 'get_data_on_screen', + contexts: ['core'], + description: dedent(`Get data that is on the screen: + ${allData.map((data) => `${data.name}: ${data.description}`).join('\n')} + `), + visibility: FunctionVisibility.AssistantOnly, + parameters: { + type: 'object', + additionalProperties: false, + additionalItems: false, + properties: { + data: { + type: 'array', + description: + 'The pieces of data you want to look at it. You can request one, or multiple', + items: { + type: 'string', + enum: allData.map((data) => data.name), + }, + additionalItems: false, + additionalProperties: false, + }, + }, + required: ['data' as const], + }, + }, + async ({ arguments: { data: dataNames } }) => { + return { + content: allData.filter((data) => dataNames.includes(data.name)), + }; + } + ); + } + } + + registerFunction: RegisterFunction = (definition, respond) => { + this.validators.set(definition.name, ajv.compile(definition.parameters)); + this.functionRegistry.set(definition.name, { definition, respond }); + }; + + registerContext: RegisterContextDefinition = (context) => { + this.contextRegistry.set(context.name, context); + }; private validate(name: string, parameters: unknown) { const validator = this.validators.get(name)!; @@ -89,6 +145,9 @@ export class ChatFunctionClient { this.validate(name, parsedArguments); - return await fn.respond({ arguments: parsedArguments, messages, connectorId }, signal); + return await fn.respond( + { arguments: parsedArguments, messages, connectorId, appContexts: this.appContexts }, + signal + ); } } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts index f3671d56a5784..8ad4ff593133a 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts @@ -1130,7 +1130,7 @@ describe('Observability AI Assistant client', () => { role: MessageRole.Assistant, function_call: { name: 'context', - arguments: JSON.stringify({ queries: [], contexts: [] }), + arguments: JSON.stringify({ queries: [], categories: [] }), trigger: MessageRole.Assistant, }, }, diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index e7a88343d16b4..2f37267505ff0 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -38,7 +38,6 @@ import { FunctionResponse, FunctionVisibility, MessageRole, - type ChatContext, type CompatibleJSONSchema, type Conversation, type ConversationCreateRequest, @@ -60,8 +59,6 @@ import { getAccessQuery } from '../util/get_access_query'; import { streamIntoObservable } from '../util/stream_into_observable'; export class ObservabilityAIAssistantClient { - chatContext: ChatContext = {}; - constructor( private readonly dependencies: { actionsClient: PublicMethodsOf; @@ -203,6 +200,20 @@ export class ObservabilityAIAssistantClient { return await next(nextMessages.concat(addedMessage)); } else if (isUserMessage) { + const functions = + numFunctionsCalled >= MAX_FUNCTION_CALLS + ? [] + : functionClient + .getFunctions() + .filter((fn) => { + const visibility = fn.definition.visibility ?? FunctionVisibility.All; + return ( + visibility === FunctionVisibility.All || + visibility === FunctionVisibility.AssistantOnly + ); + }) + .map((fn) => pick(fn.definition, 'name', 'description', 'parameters')); + const response$ = ( await this.chat( lastMessage.message.name && lastMessage.message.name !== 'recall' @@ -212,19 +223,7 @@ export class ObservabilityAIAssistantClient { messages: nextMessages, connectorId, signal, - functions: - numFunctionsCalled >= MAX_FUNCTION_CALLS - ? [] - : functionClient - .getFunctions() - .filter((fn) => { - const visibility = fn.definition.visibility ?? FunctionVisibility.All; - return ( - visibility === FunctionVisibility.All || - visibility === FunctionVisibility.AssistantOnly - ); - }) - .map((fn) => pick(fn.definition, 'name', 'description', 'parameters')), + functions, } ) ).pipe(emitWithConcatenatedMessage(), shareReplay()); @@ -778,16 +777,4 @@ export class ObservabilityAIAssistantClient { deleteKnowledgeBaseEntry = async (id: string) => { return this.dependencies.knowledgeBaseService.deleteEntry({ id }); }; - - setChatContext = (newContext: ChatContext) => { - this.chatContext = { ...this.chatContext, ...newContext }; - }; - - getChatContext = async () => { - return this.chatContext; - }; - - clearChatContext = () => { - this.chatContext = {}; - }; } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts index 324ee3ed26a00..3c4dc6f9f1bfb 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -12,13 +12,8 @@ import type { CoreSetup, CoreStart, KibanaRequest, Logger } from '@kbn/core/serv import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; -import Ajv, { type ValidateFunction } from 'ajv'; import { once } from 'lodash'; -import { - ContextRegistry, - KnowledgeBaseEntryRole, - RegisterContextDefinition, -} from '../../common/types'; +import { KnowledgeBaseEntryRole, ObservabilityAIAssistantAppContext } from '../../common/types'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../types'; import { ChatFunctionClient } from './chat_function_client'; import { ObservabilityAIAssistantClient } from './client'; @@ -27,17 +22,11 @@ import { kbComponentTemplate } from './kb_component_template'; import { KnowledgeBaseEntryOperationType, KnowledgeBaseService } from './knowledge_base_service'; import type { ChatRegistrationFunction, - FunctionHandlerRegistry, ObservabilityAIAssistantResourceNames, - RegisterFunction, RespondFunctionResources, } from './types'; import { splitKbText } from './util/split_kb_text'; -const ajv = new Ajv({ - strict: false, -}); - function getResourceName(resource: string) { return `.kibana-observability-ai-assistant-${resource}`; } @@ -297,37 +286,36 @@ export class ObservabilityAIAssistantService { } async getFunctionClient({ + appContexts, signal, resources, client, }: { + appContexts: ObservabilityAIAssistantAppContext[]; signal: AbortSignal; resources: RespondFunctionResources; client: ObservabilityAIAssistantClient; }): Promise { - const contextRegistry: ContextRegistry = new Map(); - const functionHandlerRegistry: FunctionHandlerRegistry = new Map(); - - const validators = new Map(); - - const registerContext: RegisterContextDefinition = (context) => { - contextRegistry.set(context.name, context); + const fnClient = new ChatFunctionClient(appContexts); + + const params = { + signal, + registerContext: fnClient.registerContext.bind(fnClient), + registerFunction: fnClient.registerFunction.bind(fnClient), + resources, + client, }; - const registerFunction: RegisterFunction = (definition, respond) => { - validators.set(definition.name, ajv.compile(definition.parameters)); - functionHandlerRegistry.set(definition.name, { definition, respond }); - }; await Promise.all( this.registrations.map((fn) => - fn({ signal, registerContext, registerFunction, resources, client }).catch((error) => { + fn(params).catch((error) => { this.logger.error(`Error registering functions`); this.logger.error(error); }) ) ); - return new ChatFunctionClient(contextRegistry, functionHandlerRegistry, validators); + return fnClient; } addToKnowledgeBase(entries: KnowledgeBaseEntryRequest[]): void { diff --git a/x-pack/plugins/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_ai_assistant/server/service/types.ts index d89bfd546c702..c6ece87ae35f4 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/types.ts @@ -11,6 +11,7 @@ import type { FunctionDefinition, FunctionResponse, Message, + ObservabilityAIAssistantAppContext, RegisterContextDefinition, } from '../../common/types'; import type { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types'; @@ -26,6 +27,7 @@ type RespondFunction = ( arguments: TArguments; messages: Message[]; connectorId: string; + appContexts: ObservabilityAIAssistantAppContext[]; }, signal: AbortSignal ) => Promise; From e5c77c861462dbec3d8132b6966ccbc2e51f11f7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 12:44:39 +0100 Subject: [PATCH 06/17] Rename recall to context for Bedrock as well --- .../service/client/adapters/bedrock_claude_adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts index d5ba0d726ab12..da1ba9cb9fdc4 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/adapters/bedrock_claude_adapter.ts @@ -54,9 +54,9 @@ export const createBedrockClaudeAdapter: LlmApiAdapterFactory = ({ message, consider whether it still makes sense to follow it up with another function call. ${ - functions?.find((fn) => fn.name === 'recall') - ? `The "recall" function is ALWAYS used after a user question. Even if it was used before, your job is to answer the last user question, - even if the "recall" function was executed after that. Consider the tools you need to answer the user's question.` + functions?.find((fn) => fn.name === 'context') + ? `The "context" function is ALWAYS used after a user question. Even if it was used before, your job is to answer the last user question, + even if the "context" function was executed after that. Consider the tools you need to answer the user's question.` : '' } From 5631579662ba526cb19842e0f462364920f02885 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:30:14 +0000 Subject: [PATCH 07/17] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../observability_ai_assistant/public/service/create_service.ts | 2 +- x-pack/plugins/observability_ai_assistant/public/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index 62e60b15925a1..e8c9b092bf1f2 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -33,7 +33,7 @@ export function createService({ const registrations: ChatRegistrationRenderFunction[] = []; - let appContexts: Array = []; + const appContexts: ObservabilityAIAssistantAppContext[] = []; return { isEnabled: () => { diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 4fa88decb3ee4..440c5a7149d27 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -41,7 +41,7 @@ import type { ObservabilityAIAssistantAppContext, PendingMessage, } from '../common/types'; -import type { ChatActionClickHandler, ChatFlyoutSecondSlotHandler } from './components/chat/types'; +import type { ChatActionClickHandler } from './components/chat/types'; import type { ObservabilityAIAssistantAPIClient } from './api'; import type { InsightProps } from './components/insight/insight'; import type { UseGenAIConnectorsResult } from './hooks/use_genai_connectors'; From 998bfa406a80af8e8a6858aa718070a79e1add9c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 17:02:22 +0100 Subject: [PATCH 08/17] Review feedback, fix tests --- .../apm/public/components/app/service_inventory/index.tsx | 8 ++++---- .../components/routing/templates/apm_main_template.tsx | 8 ++++---- .../routing/templates/settings_template.stories.tsx | 6 ++++++ .../public/context/apm_plugin/mock_apm_plugin_context.tsx | 7 ++++++- .../context/apm_plugin/mock_apm_plugin_storybook.tsx | 7 ++++++- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 33a5760d0eb22..a9b37c99057c8 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -18,7 +18,7 @@ import { } from '../../../../common/service_inventory'; import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { FETCH_STATUS, isFailure, isPending } from '../../../hooks/use_fetcher'; import { useLocalStorage } from '../../../hooks/use_local_storage'; import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size'; import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher'; @@ -271,13 +271,13 @@ export function ServiceInventory() { .setApplicationContext; useEffect(() => { - if (mainStatisticsStatus === FETCH_STATUS.FAILURE) { + if (isFailure(mainStatisticsStatus)) { return setApplicationContext({ description: 'The services have failed to load', }); } - if (mainStatisticsStatus === FETCH_STATUS.LOADING) { + if (isPending(mainStatisticsStatus)) { return setApplicationContext({ description: 'The services are still loading', }); @@ -292,7 +292,7 @@ export function ServiceInventory() { }, ], }); - }, [mainStatisticsStatus, mainStatisticsData.items]); + }, [mainStatisticsStatus, mainStatisticsData.items, setApplicationContext]); return ( <> diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index 8f5963eb55bea..27624d6509f2b 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -127,12 +127,12 @@ export function ApmMainTemplate({ hasApmIntegrations ? 'The user has the APM integration installed. ' : 'The user does not have the APM integration installed', - !noDataConfig - ? '' - : 'The user is looking at a screen that tells them they do not have any data.', + noDataConfig !== undefined + ? 'The user is looking at a screen that tells them they do not have any data.' + : '', ].join('\n'), }); - }, [hasApmData, hasApmIntegrations, !!noDataConfig]); + }, [hasApmData, hasApmIntegrations, noDataConfig, aiAssistant]); const rightSideItems = [ ...(showServiceGroupSaveButton ? [] : []), diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx index e3604e67ebf5b..bdb28594a52c1 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx @@ -7,6 +7,7 @@ import type { CoreStart } from '@kbn/core/public'; import type { Meta, Story } from '@storybook/react'; +import { noop } from 'lodash'; import React, { ComponentProps } from 'react'; import type { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { MockApmPluginStorybook } from '../../../context/apm_plugin/mock_apm_plugin_storybook'; @@ -23,6 +24,11 @@ const coreMock = { }, }, }, + observabilityAIAssistant: { + service: { + setApplicationContext: () => noop, + }, + }, } as unknown as Partial; const configMock = { diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 8c306354aa4ff..a4b5abe9d122e 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -9,7 +9,7 @@ import React, { ReactNode, useMemo } from 'react'; import { RouterProvider } from '@kbn/typed-react-router-config'; import { useHistory } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; -import { merge } from 'lodash'; +import { merge, noop } from 'lodash'; import { coreMock } from '@kbn/core/public/mocks'; import { UrlService } from '@kbn/share-plugin/common/url_service'; import { createObservabilityRuleTypeRegistryMock } from '@kbn/observability-plugin/public'; @@ -171,6 +171,11 @@ export const mockApmPluginContextValue = { uiActions: { getTriggerCompatibleActions: () => Promise.resolve([]), }, + observabilityAIAssistant: { + service: { + setApplicationContext: jest.fn().mockImplementation(() => noop), + }, + }, }; export function MockApmPluginContextWrapper({ diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx index af57e9de63c31..3be86811f7179 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx @@ -13,7 +13,7 @@ import { UI_SETTINGS } from '@kbn/observability-shared-plugin/public/hooks/use_k import { UrlService } from '@kbn/share-plugin/common/url_service'; import { RouterProvider } from '@kbn/typed-react-router-config'; import { createMemoryHistory } from 'history'; -import { merge } from 'lodash'; +import { merge, noop } from 'lodash'; import React, { ReactNode } from 'react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { Observable, of } from 'rxjs'; @@ -128,6 +128,11 @@ const mockCore = { const mockApmPluginContext = { core: mockCore, plugins: mockPlugin, + observabilityAIAssistant: { + service: { + setApplicationContext: () => noop, + }, + }, } as unknown as ApmPluginContextValue; export function MockApmPluginStorybook({ From 28269fef0b7ca9db96e9cc8af0a7c51e70f2bda2 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 17:17:01 +0100 Subject: [PATCH 09/17] Fix API test --- .../tests/complete/complete.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts index 82ad5b6dd1224..838c078ffb473 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts @@ -91,6 +91,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { messages, connectorId, persist: false, + appContexts: [], }) .pipe(passThrough); @@ -167,6 +168,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { messages, connectorId, persist: true, + appContexts: [], }) .end((err, response) => { if (err) { From f0fad69154d6a9f490bc99a9a23c9ec346fb8270 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 18:03:00 +0100 Subject: [PATCH 10/17] Fix types --- .../observability_ai_assistant_multipane_flyout_provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx index 93a091ff4a7d4..4ec1faa18b274 100644 --- a/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/context/observability_ai_assistant_multipane_flyout_provider.tsx @@ -6,7 +6,7 @@ */ import { createContext } from 'react'; -import type { ChatFlyoutSecondSlotHandler } from '../types'; +import type { ChatFlyoutSecondSlotHandler } from '../components/chat/types'; export const ObservabilityAIAssistantMultipaneFlyoutContext = createContext< ChatFlyoutSecondSlotHandler | undefined From 91d4c2589aa84e142b7636c13e935bee6e7df82a Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 9 Feb 2024 19:17:42 +0100 Subject: [PATCH 11/17] Lint errors --- .../apm/public/components/app/transaction_overview/index.tsx | 2 +- .../public/components/action_menu_item/action_menu_item.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 6859957092906..32fb1fc73f96a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -64,7 +64,7 @@ export function TransactionOverview() { return setApplicationContext({ description: `The user is looking at the transactions overview for ${serviceName}, and the transaction type is ${transactionType}`, }); - }, [setApplicationContext]); + }, [setApplicationContext, serviceName, transactionType]); return ( <> diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index 3e3aa7d4e387b..d1a6fb4c6ff05 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -52,7 +52,7 @@ export function ObservabilityAIAssistantActionMenuItem() { return () => { unregister(); }; - }, []); + }, [service]); if (!service.isEnabled()) { return null; From 96b9a4e56f743e6c059cf15947eb8e7b08dd06bf Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 12 Feb 2024 18:05:13 +0100 Subject: [PATCH 12/17] Add SLO to screen data --- .../pages/alert_details/alert_details.tsx | 37 +++++ .../public/pages/slo_details/slo_details.tsx | 31 +++- .../public/pages/slos/components/slo_list.tsx | 47 +++++- .../server/functions/context.ts | 138 ++++++++++++------ .../server/functions/index.ts | 19 ++- .../query/correct_common_esql_mistakes.ts | 2 +- .../server/functions/query/index.ts | 6 +- .../server/service/client/index.test.ts | 10 +- .../server/service/client/index.ts | 54 +++---- .../server/service/index.ts | 1 + .../server/service/types.ts | 2 + .../util/create_function_request_message.ts | 37 +++++ .../util/create_function_response_message.ts | 37 +++++ 13 files changed, 330 insertions(+), 91 deletions(-) create mode 100644 x-pack/plugins/observability_ai_assistant/server/service/util/create_function_request_message.ts create mode 100644 x-pack/plugins/observability_ai_assistant/server/service/util/create_function_response_message.ts diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx index 6ce338358c646..c43b29278d990 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -20,6 +20,7 @@ import { import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import dedent from 'dedent'; import { useKibana } from '../../utils/kibana_react'; import { useFetchRule } from '../../hooks/use_fetch_rule'; import { usePluginContext } from '../../hooks/use_plugin_context'; @@ -56,6 +57,9 @@ export function AlertDetails() { }, http, triggersActionsUi: { ruleTypeRegistry }, + observabilityAIAssistant: { + service: { setApplicationContext }, + }, uiSettings, } = useKibana().services; @@ -71,6 +75,39 @@ export function AlertDetails() { const [summaryFields, setSummaryFields] = useState(); const [alertStatus, setAlertStatus] = useState(); + useEffect(() => { + if (!alertDetail) { + return; + } + + const description = dedent(`The user is looking at an ${ + alertDetail.formatted.active ? 'active' : 'recovered' + } alert. + It started at ${new Date( + alertDetail.formatted.start + ).toISOString()}, and was last updated at ${new Date( + alertDetail.formatted.lastUpdated + ).toISOString()}. + + ${ + alertDetail.formatted.reason + ? `The reason given for the alert is ${alertDetail.formatted.reason}.` + : '' + } + `); + + return setApplicationContext({ + description, + data: [ + { + name: 'alert_fields', + description: 'The fields and values for the alert', + value: alertDetail.formatted.fields, + }, + ], + }); + }, [setApplicationContext, alertDetail]); + useEffect(() => { if (alertDetail) { setRuleTypeModel(ruleTypeRegistry.get(alertDetail?.formatted.fields[ALERT_RULE_TYPE_ID]!)); diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index 868b90ea5549e..7409391b7b4d1 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import { useIsMutating } from '@tanstack/react-query'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -15,6 +15,7 @@ import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; import type { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import dedent from 'dedent'; import { useKibana } from '../../utils/kibana_react'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; @@ -34,6 +35,9 @@ export function SloDetailsPage() { const { application: { navigateToUrl }, http: { basePath }, + observabilityAIAssistant: { + service: { setApplicationContext }, + }, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); @@ -53,6 +57,31 @@ export function SloDetailsPage() { useBreadcrumbs(getBreadcrumbs(basePath, slo)); + useEffect(() => { + if (!slo) { + return; + } + + return setApplicationContext({ + description: dedent(` + The user is looking at the detail page for the following SLO + + Name: ${slo.name}. + Id: ${slo.id} + Description: ${slo.description} + Observed value: ${slo.summary.sliValue} + Status: ${slo.summary.status} + `), + data: [ + { + name: 'slo', + description: 'The SLO and its metadata', + value: slo, + }, + ], + }); + }, [setApplicationContext, slo]); + const isSloNotFound = !isLoading && slo === undefined; if (isSloNotFound) { return ; diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 66a8ea84fcce8..7520d2a997a70 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -7,12 +7,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiTablePagination } from '@elastic/eui'; import { useIsMutating } from '@tanstack/react-query'; -import React from 'react'; +import React, { useEffect } from 'react'; +import dedent from 'dedent'; +import { groupBy as _groupBy, mapValues } from 'lodash'; import { useFetchSloList } from '../../../hooks/slo/use_fetch_slo_list'; import { SearchState, useUrlSearchState } from '../hooks/use_url_search_state'; import { SlosView } from './slos_view'; import { ToggleSLOView } from './toggle_slo_view'; import { GroupView } from './grouped_slos/group_view'; +import { useKibana } from '../../../utils/kibana_react'; export function SloList() { const { state, onStateChange: storeState } = useUrlSearchState(); @@ -34,6 +37,12 @@ export function SloList() { sortDirection: state.sort.direction, lastRefresh: state.lastRefresh, }); + + const { + observabilityAIAssistant: { + service: { setApplicationContext }, + }, + } = useKibana().services; const { results = [], total = 0 } = sloList ?? {}; const isCreatingSlo = Boolean(useIsMutating(['creatingSlo'])); @@ -45,6 +54,42 @@ export function SloList() { storeState({ page: 0, ...newState }); }; + useEffect(() => { + if (!sloList) { + return; + } + + const slosByStatus = mapValues( + _groupBy(sloList.results, (result) => result.summary.status), + (groupResults) => groupResults.map((result) => `- ${result.name}`).join('\n') + ) as Record; + + return setApplicationContext({ + description: dedent(`The user is looking at a list of SLOs. + + ${ + sloList.total >= 1 + ? `There are ${sloList.total} SLOs. Out of those, ${sloList.results.length} are visible. + + Violating SLOs: + ${slosByStatus.VIOLATED} + + Degrading SLOs: + ${slosByStatus.DEGRADING} + + Healthy SLOs: + ${slosByStatus.HEALTHY} + + SLOs without data: + ${slosByStatus.NO_DATA} + + ` + : '' + } + `), + }); + }, [sloList, setApplicationContext]); + return ( diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts index 4d60aaff527ea..a3e1d46984bbb 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts @@ -6,30 +6,35 @@ */ import { decodeOrThrow, jsonRt } from '@kbn/io-ts-utils'; +import { Logger } from '@kbn/logging'; import type { Serializable } from '@kbn/utility-types'; import dedent from 'dedent'; +import { encode } from 'gpt-tokenizer'; import * as t from 'io-ts'; import { compact, last, omit } from 'lodash'; -import { lastValueFrom } from 'rxjs'; -import { Logger } from '@kbn/logging'; +import { lastValueFrom, Observable } from 'rxjs'; import { FunctionRegistrationParameters } from '.'; +import { MessageAddEvent } from '../../common/conversation_complete'; import { FunctionVisibility, MessageRole, type Message } from '../../common/types'; import { concatenateChatCompletionChunks } from '../../common/utils/concatenate_chat_completion_chunks'; import type { ObservabilityAIAssistantClient } from '../service/client'; +import { createFunctionResponseMessage } from '../service/util/create_function_response_message'; + +const MAX_TOKEN_COUNT_FOR_DATA_ON_SCREEN = 1000; export function registerContextFunction({ client, registerFunction, resources, -}: FunctionRegistrationParameters) { + isKnowledgeBaseAvailable, +}: FunctionRegistrationParameters & { isKnowledgeBaseAvailable: boolean }) { registerFunction( { name: 'context', contexts: ['core'], - description: '', - visibility: FunctionVisibility.Internal, - descriptionForUser: - 'This function allows the assistant to recall previous learnings from the Knowledge base and gather context of how you are using the application.', + description: + 'This function provides context as to what the user is looking at on their screen, and recalled documents from the knowledge base that matches their query', + visibility: FunctionVisibility.AssistantOnly, parameters: { type: 'object', additionalProperties: false, @@ -58,51 +63,92 @@ export function registerContextFunction({ required: ['queries', 'categories'], } as const, }, - async ({ arguments: { queries, categories }, messages, connectorId, appContexts }, signal) => { - const systemMessage = messages.find((message) => message.message.role === MessageRole.System); - - if (!systemMessage) { - throw new Error('No system message found'); - } - - const userMessage = last( - messages.filter((message) => message.message.role === MessageRole.User) - ); - - const nonEmptyQueries = compact(queries); + async ({ arguments: args, messages, connectorId, appContexts }, signal) => { + const { queries, categories } = args; + + async function getContext() { + const systemMessage = messages.find( + (message) => message.message.role === MessageRole.System + ); + + const screenDescription = appContexts.map((context) => context.description).join('\n\n'); + + const content = { screen_description: screenDescription, learnings: [] }; + + if (!isKnowledgeBaseAvailable) { + return { content }; + } + + if (!systemMessage) { + throw new Error('No system message found'); + } + + const userMessage = last( + messages.filter((message) => message.message.role === MessageRole.User) + ); + + const nonEmptyQueries = compact(queries); + + const queriesOrUserPrompt = nonEmptyQueries.length + ? nonEmptyQueries + : compact([userMessage?.message.content]); + + const suggestions = await retrieveSuggestions({ + userMessage, + client, + categories, + queries: queriesOrUserPrompt, + }); + + if (suggestions.length === 0) { + return { + content, + }; + } + + const relevantDocuments = await scoreSuggestions({ + suggestions, + queries: queriesOrUserPrompt, + messages, + client, + connectorId, + signal, + logger: resources.logger, + }); - const queriesOrUserPrompt = nonEmptyQueries.length - ? nonEmptyQueries - : compact([userMessage?.message.content]); - - const suggestions = await retrieveSuggestions({ - userMessage, - client, - categories, - queries: queriesOrUserPrompt, - }); - - const screenDescription = appContexts.map((context) => context.description).join('\n\n'); - - if (suggestions.length === 0) { return { - content: { learnings: [] as unknown as Serializable, screenDescription }, + content: { ...content, learnings: relevantDocuments as unknown as Serializable }, }; } - const relevantDocuments = await scoreSuggestions({ - suggestions, - queries: queriesOrUserPrompt, - messages, - client, - connectorId, - signal, - logger: resources.logger, + return new Observable((subscriber) => { + getContext() + .then(({ content }) => { + // any data that falls within the token limit, send it automatically + + const dataWithinTokenLimit = compact( + appContexts.flatMap((context) => context.data) + ).filter( + (data) => + encode(JSON.stringify(data.value)).length <= MAX_TOKEN_COUNT_FOR_DATA_ON_SCREEN + ); + + subscriber.next( + createFunctionResponseMessage({ + name: 'context', + content: { + ...content, + ...(dataWithinTokenLimit.length ? { data_on_screen: dataWithinTokenLimit } : {}), + }, + }) + ); + + subscriber.complete(); + }) + .catch((error) => { + subscriber.error(error); + }); }); - - return { - content: { learnings: relevantDocuments as unknown as Serializable, screenDescription }, - }; } ); } diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts index a3a527a451a5a..9538351f4bf4e 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/index.ts @@ -19,13 +19,14 @@ import { registerVisualizeESQLFunction } from './visualize_esql'; export type FunctionRegistrationParameters = Omit< Parameters[0], - 'registerContext' + 'registerContext' | 'hasFunction' >; export const registerFunctions: ChatRegistrationFunction = async ({ client, registerContext, registerFunction, + hasFunction, resources, signal, }) => { @@ -53,7 +54,6 @@ export const registerFunctions: ChatRegistrationFunction = async ({ If multiple functions are suitable, use the most specific and easy one. E.g., when the user asks to visualise APM data, use the APM functions (if available) rather than "query". - Note that ES|QL (the Elasticsearch query language, which is NOT Elasticsearch SQL, but a new piped language) is the preferred query language. If the user wants to visualize data, or run any arbitrary query, always use the "query" function. DO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries @@ -69,8 +69,14 @@ export const registerFunctions: ChatRegistrationFunction = async ({ If the "get_dataset_info" function returns no data, and the user asks for a query, generate a query anyway with the "query" function, but be explicit about it potentially being incorrect. - You have access to data on the screen by calling the "get_data_on_screen" function if it is available. Use it to help the user understand what they are looking at. - A short summary of what they are looking at is available in the return of the "context" function. + ${ + hasFunction('get_data_on_screen') + ? `You have access to data on the screen by calling the "get_data_on_screen" function. + Use it to help the user understand what they are looking at. A short summary of what they are looking at is available in the return of the "context" function. + Data that is compact enough automatically gets included in the response for the "context" function. + ` + : '' + } ` ); @@ -82,16 +88,17 @@ export const registerFunctions: ChatRegistrationFunction = async ({ `; registerSummarizationFunction(registrationParameters); - registerContextFunction(registrationParameters); registerLensFunction(registrationParameters); - registerVisualizeESQLFunction(registrationParameters); } else { description += `You do not have a working memory. If the user expects you to remember the previous conversations, tell them they can set up the knowledge base.`; } + registerContextFunction({ ...registrationParameters, isKnowledgeBaseAvailable: isReady }); + registerElasticsearchFunction(registrationParameters); registerKibanaFunction(registrationParameters); registerQueryFunction(registrationParameters); + registerVisualizeESQLFunction(registrationParameters); registerAlertsFunction(registrationParameters); registerGetDatasetInfoFunction(registrationParameters); diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.ts b/x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.ts index 11fcd3928695b..b0877dede3a84 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/query/correct_common_esql_mistakes.ts @@ -178,7 +178,7 @@ export function correctCommonEsqlMistakes(content: string, log: Logger) { const correctedFormattedQuery = formattedCommands.join('\n| '); - const originalFormattedQuery = commands.join('\n| '); + const originalFormattedQuery = commands.map((cmd) => cmd.command).join('\n| '); if (originalFormattedQuery !== correctedFormattedQuery) { log.debug(`Modified query from: ${originalFormattedQuery}\nto:\n${correctedFormattedQuery}`); diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts b/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts index 86da0c0395587..fb6f89db825e0 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/query/index.ts @@ -9,7 +9,7 @@ import Fs from 'fs'; import { keyBy, mapValues, once, pick } from 'lodash'; import pLimit from 'p-limit'; import Path from 'path'; -import { lastValueFrom, type Observable } from 'rxjs'; +import { lastValueFrom, startWith, type Observable } from 'rxjs'; import { promisify } from 'util'; import type { FunctionRegistrationParameters } from '..'; import type { ChatCompletionChunkEvent } from '../../../common/conversation_complete'; @@ -20,6 +20,7 @@ import { import { FunctionVisibility, MessageRole } from '../../../common/types'; import { concatenateChatCompletionChunks } from '../../../common/utils/concatenate_chat_completion_chunks'; import { emitWithConcatenatedMessage } from '../../../common/utils/emit_with_concatenated_message'; +import { createFunctionResponseMessage } from '../../service/util/create_function_response_message'; import { correctCommonEsqlMistakes } from './correct_common_esql_mistakes'; const readFile = promisify(Fs.readFile); @@ -339,7 +340,8 @@ export function registerQueryFunction({ : {}), }, }; - }) + }), + startWith(createFunctionResponseMessage({ name: 'query', content: { switch: true } })) ); } ); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts index 525a66a20885d..1c7ecb99a9ebe 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.test.ts @@ -26,6 +26,7 @@ import { import type { CreateChatCompletionResponseChunk } from '../../../public/types'; import type { ChatFunctionClient } from '../chat_function_client'; import type { KnowledgeBaseService } from '../knowledge_base_service'; +import { createFunctionResponseMessage } from '../util/create_function_response_message'; import { observableIntoStream } from '../util/observable_into_stream'; type ChunkDelta = CreateChatCompletionResponseChunk['choices'][number]['delta']; @@ -986,11 +987,14 @@ describe('Observability AI Assistant client', () => { beforeEach(async () => { response$ = new Subject(); fnResponseResolve(response$); - await waitForNextWrite(stream); + + await nextTick(); + + response$.next(createFunctionResponseMessage({ name: 'my-function', content: {} })); }); - it('appends the function response', () => { - expect(JSON.parse(dataHandler.mock.lastCall!)).toEqual({ + it('appends the function response', async () => { + expect(JSON.parse(dataHandler.mock.calls[2]!)).toEqual({ type: StreamingChatResponseEventType.MessageAdd, id: expect.any(String), message: { diff --git a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts index ffb1e239b2fea..70f1f250859b5 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/client/index.ts @@ -313,37 +313,7 @@ export class ObservabilityAIAssistantClient { return; } - const functionResponseIsObservable = isObservable(functionResponse); - - const functionResponseMessage = { - '@timestamp': new Date().toISOString(), - message: { - name: lastMessage.message.function_call!.name, - ...(functionResponseIsObservable - ? { content: '{}' } - : { - content: JSON.stringify(functionResponse.content || {}), - data: functionResponse.data - ? JSON.stringify(functionResponse.data) - : undefined, - }), - role: MessageRole.User, - }, - }; - - this.dependencies.logger.debug( - `Function response: ${JSON.stringify(functionResponseMessage, null, 2)}` - ); - - nextMessages = nextMessages.concat(functionResponseMessage); - - subscriber.next({ - type: StreamingChatResponseEventType.MessageAdd, - message: functionResponseMessage, - id: v4(), - }); - - if (functionResponseIsObservable) { + if (isObservable(functionResponse)) { const shared = functionResponse.pipe(shareReplay()); shared.subscribe({ @@ -367,6 +337,28 @@ export class ObservabilityAIAssistantClient { return await next(nextMessages.concat(messageEvents.map((event) => event.message))); } + const functionResponseMessage = { + '@timestamp': new Date().toISOString(), + message: { + name: lastMessage.message.function_call!.name, + + content: JSON.stringify(functionResponse.content || {}), + data: functionResponse.data ? JSON.stringify(functionResponse.data) : undefined, + role: MessageRole.User, + }, + }; + + this.dependencies.logger.debug( + `Function response: ${JSON.stringify(functionResponseMessage, null, 2)}` + ); + nextMessages = nextMessages.concat(functionResponseMessage); + + subscriber.next({ + type: StreamingChatResponseEventType.MessageAdd, + message: functionResponseMessage, + id: v4(), + }); + span?.end(); return await next(nextMessages); diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts index 3c4dc6f9f1bfb..7a59b9e81c3b7 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -302,6 +302,7 @@ export class ObservabilityAIAssistantService { signal, registerContext: fnClient.registerContext.bind(fnClient), registerFunction: fnClient.registerFunction.bind(fnClient), + hasFunction: fnClient.hasFunction.bind(fnClient), resources, client, }; diff --git a/x-pack/plugins/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_ai_assistant/server/service/types.ts index c6ece87ae35f4..e543d3de445d5 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/types.ts @@ -15,6 +15,7 @@ import type { RegisterContextDefinition, } from '../../common/types'; import type { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types'; +import { ChatFunctionClient } from './chat_function_client'; import type { ObservabilityAIAssistantClient } from './client'; export type RespondFunctionResources = Pick< @@ -53,6 +54,7 @@ export type ChatRegistrationFunction = ({}: { client: ObservabilityAIAssistantClient; registerFunction: RegisterFunction; registerContext: RegisterContextDefinition; + hasFunction: ChatFunctionClient['hasFunction']; }) => Promise; export interface ObservabilityAIAssistantResourceNames { diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/create_function_request_message.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/create_function_request_message.ts new file mode 100644 index 0000000000000..8c38b03040794 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/create_function_request_message.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 } from 'uuid'; +import { MessageRole } from '../../../common'; +import { + MessageAddEvent, + StreamingChatResponseEventType, +} from '../../../common/conversation_complete'; + +export function createFunctionRequestMessage({ + name, + args, +}: { + name: string; + args: unknown; +}): MessageAddEvent { + return { + id: v4(), + type: StreamingChatResponseEventType.MessageAdd as const, + message: { + '@timestamp': new Date().toISOString(), + message: { + function_call: { + name, + arguments: JSON.stringify(args), + trigger: MessageRole.Assistant as const, + }, + role: MessageRole.Assistant, + }, + }, + }; +} diff --git a/x-pack/plugins/observability_ai_assistant/server/service/util/create_function_response_message.ts b/x-pack/plugins/observability_ai_assistant/server/service/util/create_function_response_message.ts new file mode 100644 index 0000000000000..186ff117734c3 --- /dev/null +++ b/x-pack/plugins/observability_ai_assistant/server/service/util/create_function_response_message.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { v4 } from 'uuid'; +import { MessageRole } from '../../../common'; +import { + type MessageAddEvent, + StreamingChatResponseEventType, +} from '../../../common/conversation_complete'; + +export function createFunctionResponseMessage({ + name, + content, + data, +}: { + name: string; + content: unknown; + data?: unknown; +}): MessageAddEvent { + return { + id: v4(), + type: StreamingChatResponseEventType.MessageAdd as const, + message: { + '@timestamp': new Date().toISOString(), + message: { + content: JSON.stringify(content), + ...(data ? { data: JSON.stringify(data) } : {}), + name, + role: MessageRole.User, + }, + }, + }; +} From 4c932c6dba03635192d7e175588176d30a272023 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 13 Feb 2024 09:59:38 +0100 Subject: [PATCH 13/17] Fix API/functional tests --- .../tests/complete/complete.spec.ts | 54 +++++++++++++++-- .../tests/conversations/index.spec.ts | 60 ++++++++++++++++++- 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts index 838c078ffb473..2f1f94852af32 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts @@ -10,7 +10,9 @@ import { omit } from 'lodash'; import { PassThrough } from 'stream'; import expect from '@kbn/expect'; import { + ChatCompletionChunkEvent, ConversationCreateEvent, + MessageAddEvent, StreamingChatResponseEvent, StreamingChatResponseEventType, } from '@kbn/observability-ai-assistant-plugin/common/conversation_complete'; @@ -110,22 +112,63 @@ export default function ApiTest({ getService }: FtrProviderContext) { await new Promise((resolve) => passThrough.on('end', () => resolve())); - const parsedChunks = receivedChunks + const parsedEvents = receivedChunks .join('') .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => JSON.parse(line) as StreamingChatResponseEvent); - expect(parsedChunks.length).to.be(2); - expect(omit(parsedChunks[0], 'id')).to.eql({ + expect(parsedEvents.map((event) => event.type)).to.eql([ + StreamingChatResponseEventType.MessageAdd, + StreamingChatResponseEventType.MessageAdd, + StreamingChatResponseEventType.ChatCompletionChunk, + StreamingChatResponseEventType.MessageAdd, + ]); + + const messageEvents = parsedEvents.filter( + (msg): msg is MessageAddEvent => msg.type === StreamingChatResponseEventType.MessageAdd + ); + + const chunkEvents = parsedEvents.filter( + (msg): msg is ChatCompletionChunkEvent => + msg.type === StreamingChatResponseEventType.ChatCompletionChunk + ); + + expect(omit(messageEvents[0], 'id', 'message.@timestamp')).to.eql({ + type: StreamingChatResponseEventType.MessageAdd, + message: { + message: { + content: '', + role: MessageRole.Assistant, + function_call: { + name: 'context', + arguments: JSON.stringify({ queries: [], categories: [] }), + trigger: MessageRole.Assistant, + }, + }, + }, + }); + + expect(omit(messageEvents[1], 'id', 'message.@timestamp')).to.eql({ + type: StreamingChatResponseEventType.MessageAdd, + message: { + message: { + role: MessageRole.User, + name: 'context', + content: JSON.stringify({ screen_description: '', learnings: [] }), + }, + }, + }); + + expect(omit(chunkEvents[0], 'id')).to.eql({ type: StreamingChatResponseEventType.ChatCompletionChunk, message: { content: 'Hello', }, }); - expect(omit(parsedChunks[1], 'id', 'message.@timestamp')).to.eql({ + expect(omit(messageEvents[2], 'id', 'message.@timestamp')).to.eql({ type: StreamingChatResponseEventType.MessageAdd, message: { message: { @@ -198,7 +241,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { .split('\n') .map((line) => line.trim()) .filter(Boolean) - .map((line) => JSON.parse(line) as StreamingChatResponseEvent); + .map((line) => JSON.parse(line) as StreamingChatResponseEvent) + .slice(2); // ignore context request/response, we're testing this elsewhere }); it('creates a new conversation', async () => { diff --git a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts index c0b2b36dfc029..66badaab026ab 100644 --- a/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts +++ b/x-pack/test/observability_ai_assistant_functional/tests/conversations/index.spec.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { pick } from 'lodash'; import type OpenAI from 'openai'; import { createLlmProxy, @@ -188,10 +189,39 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte expect(response.body.conversations.length).to.eql(1); - expect(response.body.conversations[0].messages.length).to.eql(3); - expect(response.body.conversations[0].conversation.title).to.be('My title'); + const { messages } = response.body.conversations[0]; + + expect(messages.length).to.eql(5); + + const [ + systemMessage, + firstUserMessage, + contextRequest, + contextResponse, + assistantResponse, + ] = messages.map((msg) => msg.message); + + expect(systemMessage.role).to.eql('system'); + + expect(firstUserMessage.content).to.eql('hello'); + + expect(pick(contextRequest.function_call, 'name', 'arguments')).to.eql({ + name: 'context', + arguments: JSON.stringify({ queries: [], categories: [] }), + }); + + expect(pick(contextResponse, 'name', 'content')).to.eql({ + name: 'context', + content: JSON.stringify({ screen_description: '', learnings: [] }), + }); + + expect(pick(assistantResponse, 'role', 'content')).to.eql({ + role: 'assistant', + content: 'My response', + }); + await common.waitUntilUrlIncludes( `/conversations/${response.body.conversations[0].conversation.id}` ); @@ -227,7 +257,31 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte endpoint: 'POST /internal/observability_ai_assistant/conversations', }); - expect(response.body.conversations[0].messages.length).to.eql(5); + const messages = response.body.conversations[0].messages.slice(5); + + expect(messages.length).to.eql(4); + + const [userReply, contextRequest, contextResponse, assistantResponse] = + messages.map((msg) => msg.message); + + expect(userReply.content).to.eql('hello'); + + expect(pick(contextRequest.function_call, 'name', 'arguments')).to.eql({ + name: 'context', + arguments: JSON.stringify({ queries: [], categories: [] }), + }); + + expect(pick(contextResponse, 'name', 'content')).to.eql({ + name: 'context', + content: JSON.stringify({ screen_description: '', learnings: [] }), + }); + + expect(pick(assistantResponse, 'role', 'content')).to.eql({ + role: 'assistant', + content: 'My second response', + }); + + expect(response.body.conversations[0].messages.length).to.eql(9); }); }); }); From 3bbe60829bb09eafb8f88e675091e197b4807c31 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 13 Feb 2024 10:43:27 +0100 Subject: [PATCH 14/17] Add screen description as a query --- .../observability_ai_assistant/server/functions/context.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts index a3e1d46984bbb..4fe17e834a0dd 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts @@ -93,6 +93,8 @@ export function registerContextFunction({ ? nonEmptyQueries : compact([userMessage?.message.content]); + queriesOrUserPrompt.push(screenDescription); + const suggestions = await retrieveSuggestions({ userMessage, client, From 2177d37d9ee2a8d2d07b6676ba499231358b60df Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 13 Feb 2024 11:14:07 +0100 Subject: [PATCH 15/17] Review feedback --- .../public/components/chat/chat_body.test.tsx | 2 +- .../server/functions/context.ts | 25 ++++++++----------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx index 2a3caaf423f83..cd77143e524d9 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx @@ -87,7 +87,7 @@ describe('', () => { role: 'assistant', function_call: { name: 'context', - arguments: '{"learnings": { "queries":[],"categories":[]} }', + arguments: '{"queries":[],"categories":[]}', trigger: 'assistant', }, content: '', diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts index 4fe17e834a0dd..5bcb2439e3c8a 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts @@ -72,8 +72,17 @@ export function registerContextFunction({ ); const screenDescription = appContexts.map((context) => context.description).join('\n\n'); + // any data that falls within the token limit, send it automatically - const content = { screen_description: screenDescription, learnings: [] }; + const dataWithinTokenLimit = compact(appContexts.flatMap((context) => context.data)).filter( + (data) => encode(JSON.stringify(data.value)).length <= MAX_TOKEN_COUNT_FOR_DATA_ON_SCREEN + ); + + const content = { + screen_description: screenDescription, + learnings: [], + ...(dataWithinTokenLimit.length ? { data_on_screen: dataWithinTokenLimit } : {}), + }; if (!isKnowledgeBaseAvailable) { return { content }; @@ -126,22 +135,10 @@ export function registerContextFunction({ return new Observable((subscriber) => { getContext() .then(({ content }) => { - // any data that falls within the token limit, send it automatically - - const dataWithinTokenLimit = compact( - appContexts.flatMap((context) => context.data) - ).filter( - (data) => - encode(JSON.stringify(data.value)).length <= MAX_TOKEN_COUNT_FOR_DATA_ON_SCREEN - ); - subscriber.next( createFunctionResponseMessage({ name: 'context', - content: { - ...content, - ...(dataWithinTokenLimit.length ? { data_on_screen: dataWithinTokenLimit } : {}), - }, + content, }) ); From 67e1f75a0e2f7c8fee299c6ccd5a2648409f98b3 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 13 Feb 2024 13:42:22 +0100 Subject: [PATCH 16/17] Rename setApplicationContext to setScreenContext --- .../components/app/service_inventory/index.tsx | 17 ++++++++--------- .../app/transaction_overview/index.tsx | 11 +++++------ .../routing/templates/apm_main_template.tsx | 4 ++-- .../templates/settings_template.stories.tsx | 2 +- .../shared/transactions_table/index.tsx | 9 ++++----- .../apm_plugin/mock_apm_plugin_context.tsx | 2 +- .../apm_plugin/mock_apm_plugin_storybook.tsx | 2 +- .../pages/alert_details/alert_details.tsx | 10 +++++----- .../public/pages/slo_details/slo_details.tsx | 8 ++++---- .../public/pages/slos/components/slo_list.tsx | 8 ++++---- .../observability_ai_assistant/common/types.ts | 4 ++-- .../action_menu_item/action_menu_item.tsx | 4 ++-- .../public/components/chat/chat_body.test.tsx | 2 +- .../public/hooks/use_chat.ts | 2 +- .../public/hooks/use_conversation.test.tsx | 4 ++-- .../observability_ai_assistant/public/mock.tsx | 4 ++-- .../public/service/create_chat_service.ts | 4 ++-- .../public/service/create_service.ts | 14 +++++++------- .../observability_ai_assistant/public/types.ts | 8 ++++---- .../server/functions/context.ts | 10 +++++++--- .../server/routes/chat/route.ts | 8 ++++---- .../server/routes/functions/route.ts | 2 +- .../server/routes/runtime_types.ts | 4 ++-- .../service/chat_function_client/index.test.ts | 2 +- .../service/chat_function_client/index.ts | 8 ++++---- .../server/service/index.ts | 8 ++++---- .../server/service/types.ts | 4 ++-- .../tests/complete/complete.spec.ts | 4 ++-- 28 files changed, 85 insertions(+), 84 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index a9b37c99057c8..1763dc3c878dd 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -266,24 +266,23 @@ export function ServiceInventory() { [tiebreakerField] ); - const setApplicationContext = - useApmPluginContext().observabilityAIAssistant.service - .setApplicationContext; + const { setScreenContext } = + useApmPluginContext().observabilityAIAssistant.service; useEffect(() => { if (isFailure(mainStatisticsStatus)) { - return setApplicationContext({ - description: 'The services have failed to load', + return setScreenContext({ + screenDescription: 'The services have failed to load', }); } if (isPending(mainStatisticsStatus)) { - return setApplicationContext({ - description: 'The services are still loading', + return setScreenContext({ + screenDescription: 'The services are still loading', }); } - return setApplicationContext({ + return setScreenContext({ data: [ { name: 'services', @@ -292,7 +291,7 @@ export function ServiceInventory() { }, ], }); - }, [mainStatisticsStatus, mainStatisticsData.items, setApplicationContext]); + }, [mainStatisticsStatus, mainStatisticsData.items, setScreenContext]); return ( <> diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 32fb1fc73f96a..0bc8c5e708f59 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -56,15 +56,14 @@ export function TransactionOverview() { false ); - const setApplicationContext = - useApmPluginContext().observabilityAIAssistant.service - .setApplicationContext; + const { setScreenContext } = + useApmPluginContext().observabilityAIAssistant.service; useEffect(() => { - return setApplicationContext({ - description: `The user is looking at the transactions overview for ${serviceName}, and the transaction type is ${transactionType}`, + return setScreenContext({ + screenDescription: `The user is looking at the transactions overview for ${serviceName}, and the transaction type is ${transactionType}`, }); - }, [setApplicationContext, serviceName, transactionType]); + }, [setScreenContext, serviceName, transactionType]); return ( <> diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx index 27624d6509f2b..09106b92e9618 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_main_template.tsx @@ -119,8 +119,8 @@ export function ApmMainTemplate({ }); useEffect(() => { - return aiAssistant.setApplicationContext({ - description: [ + return aiAssistant.setScreenContext({ + screenDescription: [ hasApmData ? 'The user has APM data.' : 'The user does not have APM data.', diff --git a/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx b/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx index bdb28594a52c1..c3c190bc8d27b 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/settings_template.stories.tsx @@ -26,7 +26,7 @@ const coreMock = { }, observabilityAIAssistant: { service: { - setApplicationContext: () => noop, + setScreenContext: () => noop, }, }, } as unknown as Partial; diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 93e0e60e1c404..b2b3b64e2a0c5 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -164,12 +164,11 @@ export function TransactionsTable({ }; }, [mainStatistics.maxCountExceeded, setSearchQueryDebounced]); - const setApplicationContext = - useApmPluginContext().observabilityAIAssistant.service - .setApplicationContext; + const { setScreenContext } = + useApmPluginContext().observabilityAIAssistant.service; useEffect(() => { - return setApplicationContext({ + return setScreenContext({ data: [ { name: 'top_transactions', @@ -183,7 +182,7 @@ export function TransactionsTable({ }, ], }); - }, [setApplicationContext, mainStatistics]); + }, [setScreenContext, mainStatistics]); return ( noop), + setScreenContext: jest.fn().mockImplementation(() => noop), }, }, }; diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx index 3be86811f7179..a358565663aa8 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_storybook.tsx @@ -130,7 +130,7 @@ const mockApmPluginContext = { plugins: mockPlugin, observabilityAIAssistant: { service: { - setApplicationContext: () => noop, + setScreenContext: () => noop, }, }, } as unknown as ApmPluginContextValue; diff --git a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx index c43b29278d990..6c15585dcf9f5 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -58,7 +58,7 @@ export function AlertDetails() { http, triggersActionsUi: { ruleTypeRegistry }, observabilityAIAssistant: { - service: { setApplicationContext }, + service: { setScreenContext }, }, uiSettings, } = useKibana().services; @@ -80,7 +80,7 @@ export function AlertDetails() { return; } - const description = dedent(`The user is looking at an ${ + const screenDescription = dedent(`The user is looking at an ${ alertDetail.formatted.active ? 'active' : 'recovered' } alert. It started at ${new Date( @@ -96,8 +96,8 @@ export function AlertDetails() { } `); - return setApplicationContext({ - description, + return setScreenContext({ + screenDescription, data: [ { name: 'alert_fields', @@ -106,7 +106,7 @@ export function AlertDetails() { }, ], }); - }, [setApplicationContext, alertDetail]); + }, [setScreenContext, alertDetail]); useEffect(() => { if (alertDetail) { diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index 7409391b7b4d1..1e4f590a8bf7d 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -36,7 +36,7 @@ export function SloDetailsPage() { application: { navigateToUrl }, http: { basePath }, observabilityAIAssistant: { - service: { setApplicationContext }, + service: { setScreenContext }, }, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); @@ -62,8 +62,8 @@ export function SloDetailsPage() { return; } - return setApplicationContext({ - description: dedent(` + return setScreenContext({ + screenDescription: dedent(` The user is looking at the detail page for the following SLO Name: ${slo.name}. @@ -80,7 +80,7 @@ export function SloDetailsPage() { }, ], }); - }, [setApplicationContext, slo]); + }, [setScreenContext, slo]); const isSloNotFound = !isLoading && slo === undefined; if (isSloNotFound) { diff --git a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx index 7520d2a997a70..b7b51f631860a 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/slo_list.tsx @@ -40,7 +40,7 @@ export function SloList() { const { observabilityAIAssistant: { - service: { setApplicationContext }, + service: { setScreenContext }, }, } = useKibana().services; const { results = [], total = 0 } = sloList ?? {}; @@ -64,8 +64,8 @@ export function SloList() { (groupResults) => groupResults.map((result) => `- ${result.name}`).join('\n') ) as Record; - return setApplicationContext({ - description: dedent(`The user is looking at a list of SLOs. + return setScreenContext({ + screenDescription: dedent(`The user is looking at a list of SLOs. ${ sloList.total >= 1 @@ -88,7 +88,7 @@ export function SloList() { } `), }); - }, [sloList, setApplicationContext]); + }, [sloList, setScreenContext]); return ( diff --git a/x-pack/plugins/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_ai_assistant/common/types.ts index 3c48f1657af4f..563d5aa893df8 100644 --- a/x-pack/plugins/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_ai_assistant/common/types.ts @@ -127,8 +127,8 @@ export type RegisterContextDefinition = (options: ContextDefinition) => void; export type ContextRegistry = Map; export type FunctionRegistry = Map; -export interface ObservabilityAIAssistantAppContext { - description?: string; +export interface ObservabilityAIAssistantScreenContext { + screenDescription?: string; data?: Array<{ name: string; description: string; diff --git a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx index d1a6fb4c6ff05..c22d32e7a49d7 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/action_menu_item/action_menu_item.tsx @@ -45,8 +45,8 @@ export function ObservabilityAIAssistantActionMenuItem() { }, []); useEffect(() => { - const unregister = service.setApplicationContext({ - description: 'The user is looking at ' + window.location.href, + const unregister = service.setScreenContext({ + screenDescription: 'The user is looking at ' + window.location.href, }); return () => { diff --git a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx index cd77143e524d9..6db880f536e8d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/components/chat/chat_body.test.tsx @@ -39,7 +39,7 @@ describe('', () => { role: 'assistant', function_call: { name: 'context', - arguments: '{"queries":[],"contexts":[]}', + arguments: '{"queries":[],"categories":[]}', trigger: 'assistant', }, content: '', diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts index 5504ea7847595..8d138a1150d20 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_chat.ts @@ -156,7 +156,7 @@ export function useChat({ setChatState(ChatState.Loading); const next$ = chatService.complete({ - appContexts: service.getApplicationContexts(), + screenContexts: service.getScreenContexts(), connectorId, messages: getWithSystemMessage(nextMessages, systemMessage), persist, diff --git a/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.test.tsx b/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.test.tsx index 103e91f28484a..74bd34c3d5934 100644 --- a/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.test.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/hooks/use_conversation.test.tsx @@ -43,8 +43,8 @@ const mockService: MockedService = { isEnabled: jest.fn(), start: jest.fn(), register: jest.fn(), - setApplicationContext: jest.fn(), - getApplicationContexts: jest.fn(), + setScreenContext: jest.fn(), + getScreenContexts: jest.fn(), }; const mockChatService = createMockChatService(); diff --git a/x-pack/plugins/observability_ai_assistant/public/mock.tsx b/x-pack/plugins/observability_ai_assistant/public/mock.tsx index ba57219234048..bda8beba8ba5e 100644 --- a/x-pack/plugins/observability_ai_assistant/public/mock.tsx +++ b/x-pack/plugins/observability_ai_assistant/public/mock.tsx @@ -65,8 +65,8 @@ export const mockService: ObservabilityAIAssistantService = { navigate: () => {}, } as unknown as SharePluginStart), register: () => {}, - setApplicationContext: () => noop, - getApplicationContexts: () => [], + setScreenContext: () => noop, + getScreenContexts: () => [], }; function createSetupContract(): ObservabilityAIAssistantPluginSetup { 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 d26be6ac05774..a764fbcdbad60 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 @@ -146,14 +146,14 @@ export async function createChatService({ hasRenderFunction: (name: string) => { return renderFunctionRegistry.has(name); }, - complete({ appContexts, connectorId, conversationId, messages, persist, signal }) { + complete({ screenContexts, connectorId, conversationId, messages, persist, signal }) { return new Observable((subscriber) => { client('POST /internal/observability_ai_assistant/chat/complete', { params: { body: { connectorId, conversationId, - appContexts, + screenContexts, messages, persist, }, diff --git a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts index e8c9b092bf1f2..1b6c68e6a1f61 100644 --- a/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_ai_assistant/public/service/create_service.ts @@ -10,7 +10,7 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { SecurityPluginStart } from '@kbn/security-plugin/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import { remove } from 'lodash'; -import { ObservabilityAIAssistantAppContext } from '../../common/types'; +import { ObservabilityAIAssistantScreenContext } from '../../common/types'; import { createCallObservabilityAIAssistantAPI } from '../api'; import type { ChatRegistrationRenderFunction, ObservabilityAIAssistantService } from '../types'; @@ -33,7 +33,7 @@ export function createService({ const registrations: ChatRegistrationRenderFunction[] = []; - const appContexts: ObservabilityAIAssistantAppContext[] = []; + const screenContexts: ObservabilityAIAssistantScreenContext[] = []; return { isEnabled: () => { @@ -50,14 +50,14 @@ export function createService({ getCurrentUser: () => securityStart.authc.getCurrentUser(), getLicense: () => licenseStart.license$, getLicenseManagementLocator: () => shareStart, - setApplicationContext: (context: ObservabilityAIAssistantAppContext) => { - appContexts.push(context); + setScreenContext: (context: ObservabilityAIAssistantScreenContext) => { + screenContexts.push(context); return () => { - remove(appContexts, context); + remove(screenContexts, context); }; }, - getApplicationContexts: () => { - return appContexts; + getScreenContexts: () => { + return screenContexts; }, }; } diff --git a/x-pack/plugins/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_ai_assistant/public/types.ts index 440c5a7149d27..3a4c4fbc59d7d 100644 --- a/x-pack/plugins/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_ai_assistant/public/types.ts @@ -38,7 +38,7 @@ import type { FunctionDefinition, FunctionResponse, Message, - ObservabilityAIAssistantAppContext, + ObservabilityAIAssistantScreenContext, PendingMessage, } from '../common/types'; import type { ChatActionClickHandler } from './components/chat/types'; @@ -63,7 +63,7 @@ export interface ObservabilityAIAssistantChatService { } ) => Observable; complete: (options: { - appContexts: ObservabilityAIAssistantAppContext[]; + screenContexts: ObservabilityAIAssistantScreenContext[]; conversationId?: string; connectorId: string; messages: Message[]; @@ -90,8 +90,8 @@ export interface ObservabilityAIAssistantService { getLicenseManagementLocator: () => SharePluginStart; start: ({}: { signal: AbortSignal }) => Promise; register: (fn: ChatRegistrationRenderFunction) => void; - setApplicationContext: (appContext: ObservabilityAIAssistantAppContext) => () => void; - getApplicationContexts: () => ObservabilityAIAssistantAppContext[]; + setScreenContext: (screenContext: ObservabilityAIAssistantScreenContext) => () => void; + getScreenContexts: () => ObservabilityAIAssistantScreenContext[]; } export type RenderFunction = (options: { diff --git a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts index 5bcb2439e3c8a..febbc4156d66d 100644 --- a/x-pack/plugins/observability_ai_assistant/server/functions/context.ts +++ b/x-pack/plugins/observability_ai_assistant/server/functions/context.ts @@ -63,7 +63,7 @@ export function registerContextFunction({ required: ['queries', 'categories'], } as const, }, - async ({ arguments: args, messages, connectorId, appContexts }, signal) => { + async ({ arguments: args, messages, connectorId, screenContexts }, signal) => { const { queries, categories } = args; async function getContext() { @@ -71,10 +71,14 @@ export function registerContextFunction({ (message) => message.message.role === MessageRole.System ); - const screenDescription = appContexts.map((context) => context.description).join('\n\n'); + const screenDescription = compact( + screenContexts.map((context) => context.screenDescription) + ).join('\n\n'); // any data that falls within the token limit, send it automatically - const dataWithinTokenLimit = compact(appContexts.flatMap((context) => context.data)).filter( + const dataWithinTokenLimit = compact( + screenContexts.flatMap((context) => context.data) + ).filter( (data) => encode(JSON.stringify(data.value)).length <= MAX_TOKEN_COUNT_FOR_DATA_ON_SCREEN ); diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts index cd95dfb35d7e2..38edf29a81ee6 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/chat/route.ts @@ -11,7 +11,7 @@ import { Readable } from 'stream'; import { flushBuffer } from '../../service/util/flush_buffer'; import { observableIntoStream } from '../../service/util/observable_into_stream'; import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route'; -import { appContextRt, messageRt } from '../runtime_types'; +import { screenContextRt, messageRt } from '../runtime_types'; const chatRoute = createObservabilityAIAssistantServerRoute({ endpoint: 'POST /internal/observability_ai_assistant/chat', @@ -84,7 +84,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ body: t.intersection([ t.type({ messages: t.array(messageRt), - appContexts: t.array(appContextRt), + screenContexts: t.array(screenContextRt), connectorId: t.string, persist: toBooleanRt, }), @@ -107,7 +107,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ } const { - body: { messages, connectorId, conversationId, title, persist, appContexts }, + body: { messages, connectorId, conversationId, title, persist, screenContexts }, } = params; const controller = new AbortController(); @@ -120,7 +120,7 @@ const chatCompleteRoute = createObservabilityAIAssistantServerRoute({ signal: controller.signal, resources, client, - appContexts, + screenContexts, }); const response$ = client.complete({ diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts index 6409498081f5d..b9c3e176cf22d 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/functions/route.ts @@ -39,7 +39,7 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({ signal: controller.signal, resources, client, - appContexts: [], + screenContexts: [], }); return { diff --git a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts index 1f94af80611c4..cef56f673e235 100644 --- a/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/routes/runtime_types.ts @@ -13,7 +13,7 @@ import { type ConversationUpdateRequest, type Message, MessageRole, - type ObservabilityAIAssistantAppContext, + type ObservabilityAIAssistantScreenContext, } from '../../common/types'; const serializeableRt = t.any; @@ -94,7 +94,7 @@ export const conversationRt: t.Type = t.intersection([ }), ]); -export const appContextRt: t.Type = t.partial({ +export const screenContextRt: t.Type = t.partial({ description: t.string, data: t.array( t.type({ diff --git a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts index 37030ffa1c684..9ad808a134250 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.test.ts @@ -99,7 +99,7 @@ describe('chatFunctionClient', () => { it('exposes a function that returns the requested data', async () => { const client = new ChatFunctionClient([ { - description: 'My description', + screenDescription: 'My description', data: [ { name: 'my_dummy_data', diff --git a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts index a86726174a701..bc74f2046c5d2 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/chat_function_client/index.ts @@ -15,7 +15,7 @@ import { FunctionResponse, FunctionVisibility, Message, - ObservabilityAIAssistantAppContext, + ObservabilityAIAssistantScreenContext, RegisterContextDefinition, } from '../../../common/types'; import { filterFunctionDefinitions } from '../../../common/utils/filter_function_definitions'; @@ -36,8 +36,8 @@ export class ChatFunctionClient { private readonly functionRegistry: FunctionHandlerRegistry = new Map(); private readonly validators: Map = new Map(); - constructor(private readonly appContexts: ObservabilityAIAssistantAppContext[]) { - const allData = compact(appContexts.flatMap((context) => context.data)); + constructor(private readonly screenContexts: ObservabilityAIAssistantScreenContext[]) { + const allData = compact(screenContexts.flatMap((context) => context.data)); if (allData.length) { this.registerFunction( @@ -146,7 +146,7 @@ export class ChatFunctionClient { this.validate(name, parsedArguments); return await fn.respond( - { arguments: parsedArguments, messages, connectorId, appContexts: this.appContexts }, + { arguments: parsedArguments, messages, connectorId, screenContexts: this.screenContexts }, signal ); } diff --git a/x-pack/plugins/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_ai_assistant/server/service/index.ts index 7a59b9e81c3b7..dcd0cb95de7c4 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/index.ts @@ -13,7 +13,7 @@ import type { SecurityPluginStart } from '@kbn/security-plugin/server'; import { getSpaceIdFromPath } from '@kbn/spaces-plugin/common'; import type { TaskManagerSetupContract } from '@kbn/task-manager-plugin/server'; import { once } from 'lodash'; -import { KnowledgeBaseEntryRole, ObservabilityAIAssistantAppContext } from '../../common/types'; +import { KnowledgeBaseEntryRole, ObservabilityAIAssistantScreenContext } from '../../common/types'; import type { ObservabilityAIAssistantPluginStartDependencies } from '../types'; import { ChatFunctionClient } from './chat_function_client'; import { ObservabilityAIAssistantClient } from './client'; @@ -286,17 +286,17 @@ export class ObservabilityAIAssistantService { } async getFunctionClient({ - appContexts, + screenContexts, signal, resources, client, }: { - appContexts: ObservabilityAIAssistantAppContext[]; + screenContexts: ObservabilityAIAssistantScreenContext[]; signal: AbortSignal; resources: RespondFunctionResources; client: ObservabilityAIAssistantClient; }): Promise { - const fnClient = new ChatFunctionClient(appContexts); + const fnClient = new ChatFunctionClient(screenContexts); const params = { signal, diff --git a/x-pack/plugins/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_ai_assistant/server/service/types.ts index e543d3de445d5..2215f565886bc 100644 --- a/x-pack/plugins/observability_ai_assistant/server/service/types.ts +++ b/x-pack/plugins/observability_ai_assistant/server/service/types.ts @@ -11,7 +11,7 @@ import type { FunctionDefinition, FunctionResponse, Message, - ObservabilityAIAssistantAppContext, + ObservabilityAIAssistantScreenContext, RegisterContextDefinition, } from '../../common/types'; import type { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types'; @@ -28,7 +28,7 @@ type RespondFunction = ( arguments: TArguments; messages: Message[]; connectorId: string; - appContexts: ObservabilityAIAssistantAppContext[]; + screenContexts: ObservabilityAIAssistantScreenContext[]; }, signal: AbortSignal ) => Promise; diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts index 2f1f94852af32..c4913ce3e41d2 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/complete.spec.ts @@ -93,7 +93,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { messages, connectorId, persist: false, - appContexts: [], + screenContexts: [], }) .pipe(passThrough); @@ -211,7 +211,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { messages, connectorId, persist: true, - appContexts: [], + screenContexts: [], }) .end((err, response) => { if (err) { From d87b4f915b4465f8cad613b4558b6f91cdbc1d7c Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 13 Feb 2024 13:54:38 +0100 Subject: [PATCH 17/17] Remove console.log statement --- x-pack/plugins/observability/public/plugin.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 2eb60290dbb97..1f06179f20be7 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -410,8 +410,6 @@ export class Plugin const isAiAssistantEnabled = pluginsStart.observabilityAIAssistant.service.isEnabled(); - console.log({ isAiAssistantEnabled }); - const aiAssistantLink = isAiAssistantEnabled && !Boolean(pluginsSetup.serverless) &&