From ee341d5f801ca42ed26acf0544b0bc59948d0214 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Sat, 12 Oct 2024 01:09:06 +0200 Subject: [PATCH] [Search Assistant] Use scopes to modify behavior contextually (#195785) ## Summary This actually uses the Search Assistant scope to modify the assistant's behavior depending on the context they're in. The assistant now: - Defaults to Observability mode - Is a Search assistant in the Search pages - Switches dynamically, changing available functions, prompts and instructions based on context --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + package.json | 1 + .../test_suites/core_plugins/rendering.ts | 1 + tsconfig.base.json | 2 + .../kbn-ai-assistant-common/README.md | 3 + .../packages/kbn-ai-assistant-common/index.ts | 7 + .../kbn-ai-assistant-common/jest.config.js | 19 + .../kbn-ai-assistant-common/kibana.jsonc | 5 + .../kbn-ai-assistant-common/package.json | 7 + .../kbn-ai-assistant-common/setup_tests.ts | 9 + .../kbn-ai-assistant-common/src/index.ts | 9 + .../src/types/index.ts | 8 + .../src/utils/filter_scopes.ts | 17 + .../src/utils/index.ts | 8 + .../kbn-ai-assistant-common/tsconfig.json | 19 + .../src/chat/function_list_popover.tsx | 5 +- .../src/chat/starter_prompts.tsx | 1 - .../src/conversation/conversation_view.tsx | 9 + .../kbn-ai-assistant/src/hooks/index.ts | 1 + .../src/hooks/use_conversation.test.tsx | 7 +- .../src/hooks/use_conversation.ts | 3 +- .../src/hooks/use_functions.ts | 15 + .../src/hooks/use_json_editor_model.ts | 6 +- .../kbn-ai-assistant/src/hooks/use_scope.ts | 15 + .../src/utils/create_mock_chat_service.ts | 4 + .../packages/kbn-ai-assistant/tsconfig.json | 1 + .../common/functions/types.ts | 2 +- .../common/types.ts | 4 +- .../utils/filter_function_definitions.ts | 2 +- .../public/components/insight/insight.tsx | 2 +- .../public/hooks/use_chat.test.ts | 5 +- .../public/hooks/use_chat.ts | 2 +- .../public/mock.tsx | 10 +- .../public/plugin.tsx | 7 +- .../service/create_chat_service.test.ts | 5 +- .../public/service/create_chat_service.ts | 339 +++++++++++------- .../service/create_mock_chat_service.ts | 5 +- .../public/service/create_service.ts | 36 +- .../public/service/default_starter_prompts.ts | 7 +- .../public/storybook_mock.tsx | 13 +- .../public/types.ts | 16 +- .../server/config.ts | 1 + .../server/index.ts | 2 +- .../server/routes/chat/route.ts | 2 +- .../server/routes/functions/route.ts | 2 +- .../server/routes/runtime_types.ts | 13 +- .../service/chat_function_client/index.ts | 17 +- .../server/service/client/index.ts | 2 +- .../client/operators/continue_conversation.ts | 8 +- .../server/service/index.ts | 2 +- .../server/service/types.ts | 2 +- .../observability_ai_assistant/tsconfig.json | 3 +- .../public/components/nav_control/index.tsx | 2 + .../public/hooks/is_nav_control_visible.tsx | 5 +- .../public/hooks/use_nav_control_scope.ts | 41 +++ .../scripts/evaluation/kibana_client.ts | 6 +- .../tsconfig.json | 1 + x-pack/plugins/search_assistant/kibana.jsonc | 1 + .../conversation_view_with_props.tsx | 1 + .../server/functions/index.ts | 36 ++ .../plugins/search_assistant/server/index.ts | 6 +- .../plugins/search_assistant/server/plugin.ts | 27 +- .../plugins/search_assistant/server/types.ts | 8 + x-pack/plugins/search_assistant/tsconfig.json | 3 +- .../tests/complete/functions/helpers.ts | 2 +- x-pack/test/tsconfig.json | 3 +- .../tests/complete/functions/helpers.ts | 2 +- x-pack/test_serverless/tsconfig.json | 1 + yarn.lock | 4 + 69 files changed, 623 insertions(+), 218 deletions(-) create mode 100644 x-pack/packages/kbn-ai-assistant-common/README.md create mode 100644 x-pack/packages/kbn-ai-assistant-common/index.ts create mode 100644 x-pack/packages/kbn-ai-assistant-common/jest.config.js create mode 100644 x-pack/packages/kbn-ai-assistant-common/kibana.jsonc create mode 100644 x-pack/packages/kbn-ai-assistant-common/package.json create mode 100644 x-pack/packages/kbn-ai-assistant-common/setup_tests.ts create mode 100644 x-pack/packages/kbn-ai-assistant-common/src/index.ts create mode 100644 x-pack/packages/kbn-ai-assistant-common/src/types/index.ts create mode 100644 x-pack/packages/kbn-ai-assistant-common/src/utils/filter_scopes.ts create mode 100644 x-pack/packages/kbn-ai-assistant-common/src/utils/index.ts create mode 100644 x-pack/packages/kbn-ai-assistant-common/tsconfig.json create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_functions.ts create mode 100644 x-pack/packages/kbn-ai-assistant/src/hooks/use_scope.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_scope.ts create mode 100644 x-pack/plugins/search_assistant/server/functions/index.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a528930edc2c9..e8c6eb67a55b8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,7 @@ x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/ packages/kbn-actions-types @elastic/response-ops src/plugins/advanced_settings @elastic/appex-sharedux @elastic/kibana-management x-pack/packages/kbn-ai-assistant @elastic/search-kibana +x-pack/packages/kbn-ai-assistant-common @elastic/search-kibana src/plugins/ai_assistant_management/selection @elastic/obs-knowledge-team x-pack/packages/ml/aiops_change_point_detection @elastic/ml-ui x-pack/packages/ml/aiops_common @elastic/ml-ui diff --git a/package.json b/package.json index b4ac41e63851c..f37c3391185e2 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "@kbn/actions-types": "link:packages/kbn-actions-types", "@kbn/advanced-settings-plugin": "link:src/plugins/advanced_settings", "@kbn/ai-assistant": "link:x-pack/packages/kbn-ai-assistant", + "@kbn/ai-assistant-common": "link:x-pack/packages/kbn-ai-assistant-common", "@kbn/ai-assistant-management-plugin": "link:src/plugins/ai_assistant_management/selection", "@kbn/aiops-change-point-detection": "link:x-pack/packages/ml/aiops_change_point_detection", "@kbn/aiops-common": "link:x-pack/packages/ml/aiops_common", diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 8f75b282bf470..72d1f97011274 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -350,6 +350,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observability.unsafe.thresholdRule.enabled (boolean?)', 'xpack.observability_onboarding.ui.enabled (boolean?)', 'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)', + 'xpack.observabilityAIAssistant.scope (observability?|search?)', 'share.new_version.enabled (boolean?)', 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)', /** diff --git a/tsconfig.base.json b/tsconfig.base.json index 188c96734d2ce..8584c766d8306 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,6 +16,8 @@ "@kbn/advanced-settings-plugin/*": ["src/plugins/advanced_settings/*"], "@kbn/ai-assistant": ["x-pack/packages/kbn-ai-assistant"], "@kbn/ai-assistant/*": ["x-pack/packages/kbn-ai-assistant/*"], + "@kbn/ai-assistant-common": ["x-pack/packages/kbn-ai-assistant-common"], + "@kbn/ai-assistant-common/*": ["x-pack/packages/kbn-ai-assistant-common/*"], "@kbn/ai-assistant-management-plugin": ["src/plugins/ai_assistant_management/selection"], "@kbn/ai-assistant-management-plugin/*": ["src/plugins/ai_assistant_management/selection/*"], "@kbn/aiops-change-point-detection": ["x-pack/packages/ml/aiops_change_point_detection"], diff --git a/x-pack/packages/kbn-ai-assistant-common/README.md b/x-pack/packages/kbn-ai-assistant-common/README.md new file mode 100644 index 0000000000000..c824382dd755c --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/README.md @@ -0,0 +1,3 @@ +# @kbn/ai-assistant-common + +Provides types and utils to render the AI Assistant in plugins. diff --git a/x-pack/packages/kbn-ai-assistant-common/index.ts b/x-pack/packages/kbn-ai-assistant-common/index.ts new file mode 100644 index 0000000000000..cf53082cfa4b0 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ +export * from './src'; diff --git a/x-pack/packages/kbn-ai-assistant-common/jest.config.js b/x-pack/packages/kbn-ai-assistant-common/jest.config.js new file mode 100644 index 0000000000000..6682c06e5e764 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/jest.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/packages/kbn_ai_assistant_common_src', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/packages/kbn-ai-assistant-common/src/**/*.{ts,tsx}', + '!/x-pack/packages/kbn-ai-assistant-common/src/*.test.{ts,tsx}', + ], + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/packages/kbn-ai-assistant-common'], +}; diff --git a/x-pack/packages/kbn-ai-assistant-common/kibana.jsonc b/x-pack/packages/kbn-ai-assistant-common/kibana.jsonc new file mode 100644 index 0000000000000..8babdaccbd2df --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "id": "@kbn/ai-assistant-common", + "owner": "@elastic/search-kibana", + "type": "shared-common" +} diff --git a/x-pack/packages/kbn-ai-assistant-common/package.json b/x-pack/packages/kbn-ai-assistant-common/package.json new file mode 100644 index 0000000000000..49198d7ae7535 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/package.json @@ -0,0 +1,7 @@ +{ + "name": "@kbn/ai-assistant-common", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0", + "sideEffects": false +} diff --git a/x-pack/packages/kbn-ai-assistant-common/setup_tests.ts b/x-pack/packages/kbn-ai-assistant-common/setup_tests.ts new file mode 100644 index 0000000000000..72e0edd0d07f7 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/setup_tests.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import '@testing-library/jest-dom'; diff --git a/x-pack/packages/kbn-ai-assistant-common/src/index.ts b/x-pack/packages/kbn-ai-assistant-common/src/index.ts new file mode 100644 index 0000000000000..1c7792630d1ba --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/src/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './types'; +export * from './utils'; diff --git a/x-pack/packages/kbn-ai-assistant-common/src/types/index.ts b/x-pack/packages/kbn-ai-assistant-common/src/types/index.ts new file mode 100644 index 0000000000000..9e49c73049d37 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/src/types/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export type AssistantScope = 'observability' | 'search' | 'all'; diff --git a/x-pack/packages/kbn-ai-assistant-common/src/utils/filter_scopes.ts b/x-pack/packages/kbn-ai-assistant-common/src/utils/filter_scopes.ts new file mode 100644 index 0000000000000..ff8f627b10dac --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/src/utils/filter_scopes.ts @@ -0,0 +1,17 @@ +/* + * 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 type { AssistantScope } from '../types'; + +export function filterScopes(scope?: AssistantScope) { + return function (value: T): boolean { + if (!scope || !value) { + return true; + } + return value?.scopes ? value.scopes.includes(scope) || value.scopes.includes('all') : true; + }; +} diff --git a/x-pack/packages/kbn-ai-assistant-common/src/utils/index.ts b/x-pack/packages/kbn-ai-assistant-common/src/utils/index.ts new file mode 100644 index 0000000000000..41aea996e131c --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/src/utils/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export * from './filter_scopes'; diff --git a/x-pack/packages/kbn-ai-assistant-common/tsconfig.json b/x-pack/packages/kbn-ai-assistant-common/tsconfig.json new file mode 100644 index 0000000000000..35c604906821d --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant-common/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + ] +} diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx index d24aae12fd8c6..4d185c703b2fd 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/function_list_popover.tsx @@ -22,7 +22,7 @@ import type { EuiSelectableOptionCheckedType } from '@elastic/eui/src/components import { i18n } from '@kbn/i18n'; import { FunctionVisibility } from '@kbn/observability-ai-assistant-plugin/public'; import type { FunctionDefinition } from '@kbn/observability-ai-assistant-plugin/common'; -import { useAIAssistantChatService } from '../hooks/use_ai_assistant_chat_service'; +import { useFunctions } from '../hooks/use_functions'; interface FunctionListOption { label: string; @@ -40,8 +40,7 @@ export function FunctionListPopover({ onSelectFunction: (func: string | undefined) => void; disabled: boolean; }) { - const { getFunctions } = useAIAssistantChatService(); - const functions = getFunctions(); + const functions = useFunctions(); const [functionOptions, setFunctionOptions] = useState< Array> diff --git a/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx b/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx index faaecc0024135..f24008f862431 100644 --- a/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/chat/starter_prompts.tsx @@ -31,7 +31,6 @@ const starterPromptInnerClassName = css` export function StarterPrompts({ onSelectPrompt }: { onSelectPrompt: (prompt: string) => void }) { const service = useAIAssistantAppService(); - const { connectors } = useGenAIConnectors(); if (!connectors || connectors.length === 0) { diff --git a/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx index 260a7cb5c10ed..b7d5831e14f94 100644 --- a/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/conversation/conversation_view.tsx @@ -9,6 +9,7 @@ import { css } from '@emotion/css'; import { euiThemeVars } from '@kbn/ui-theme'; import React, { useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { useKibana } from '../hooks/use_kibana'; import { ConversationList, ChatBody, ChatInlineEditingContent } from '../chat'; import { useConversationKey } from '../hooks/use_conversation_key'; @@ -26,6 +27,7 @@ interface ConversationViewProps { navigateToConversation: (nextConversationId?: string) => void; getConversationHref?: (conversationId: string) => string; newConversationHref?: string; + scope?: AssistantScope; } export const ConversationView: React.FC = ({ @@ -33,6 +35,7 @@ export const ConversationView: React.FC = ({ navigateToConversation, getConversationHref, newConversationHref, + scope, }) => { const { euiTheme } = useEuiTheme(); @@ -57,6 +60,12 @@ export const ConversationView: React.FC = ({ [service] ); + useEffect(() => { + if (scope) { + service.setScope(scope); + } + }, [scope, service]); + const { key: bodyKey, updateConversationIdInPlace } = useConversationKey(conversationId); const [secondSlotContainer, setSecondSlotContainer] = useState(null); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts index ee630d1caec82..41bb8a4906c11 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/index.ts @@ -8,3 +8,4 @@ export * from './use_ai_assistant_app_service'; export * from './use_ai_assistant_chat_service'; export * from './use_knowledge_base'; +export * from './use_scope'; diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx index 4c4ced36c8796..02c6018a4216c 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.test.tsx @@ -13,7 +13,7 @@ import { } from '@testing-library/react-hooks'; import { merge } from 'lodash'; import React, { PropsWithChildren } from 'react'; -import { Observable, of, Subject } from 'rxjs'; +import { BehaviorSubject, Observable, of, Subject } from 'rxjs'; import { MessageRole, StreamingChatResponseEventType, @@ -31,6 +31,7 @@ import { createMockChatService } from '../utils/create_mock_chat_service'; import { createUseChat } from '@kbn/observability-ai-assistant-plugin/public/hooks/use_chat'; import type { NotificationsStart } from '@kbn/core/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { AssistantScope } from '@kbn/ai-assistant-common'; let hookResult: RenderHookResult; @@ -54,7 +55,9 @@ const mockService: MockedService = { predefinedConversation$: new Observable(), }, navigate: jest.fn().mockReturnValue(of()), - scope: 'all', + scope$: new BehaviorSubject('all') as MockedService['scope$'], + setScope: jest.fn(), + getScope: jest.fn(), }; const mockChatService = createMockChatService(); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts index 744e071d5b1ba..b40507a09719e 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_conversation.ts @@ -20,6 +20,7 @@ import { useAIAssistantAppService } from './use_ai_assistant_app_service'; import { useKibana } from './use_kibana'; import { useOnce } from './use_once'; import { useAbortableAsync } from './use_abortable_async'; +import { useScope } from './use_scope'; function createNewConversation({ title = EMPTY_CONVERSATION_TITLE, @@ -61,7 +62,7 @@ export function useConversation({ onConversationUpdate, }: UseConversationProps): UseConversationResult { const service = useAIAssistantAppService(); - const { scope } = service; + const scope = useScope(); const { services: { diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_functions.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_functions.ts new file mode 100644 index 0000000000000..5c18c5c682623 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_functions.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 { useObservable } from 'react-use/lib'; +import { useAIAssistantChatService } from './use_ai_assistant_chat_service'; + +export const useFunctions = () => { + const service = useAIAssistantChatService(); + const functions = useObservable(service.functions$); + return functions || []; +}; diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts index 1b14c504d935d..6412aeb6a870e 100644 --- a/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_json_editor_model.ts @@ -7,8 +7,8 @@ import { useEffect, useMemo, useState } from 'react'; import { monaco } from '@kbn/monaco'; import { createInitializedObject } from '../utils/create_initialized_object'; -import { useAIAssistantChatService } from './use_ai_assistant_chat_service'; import { safeJsonParse } from '../utils/safe_json_parse'; +import { useFunctions } from './use_functions'; const { editor, languages, Uri } = monaco; @@ -19,9 +19,9 @@ export const useJsonEditorModel = ({ functionName: string | undefined; initialJson?: string | undefined; }) => { - const chatService = useAIAssistantChatService(); + const functions = useFunctions(); - const functionDefinition = chatService.getFunctions().find((func) => func.name === functionName); + const functionDefinition = functions.find((func) => func.name === functionName); const [initialJsonValue, setInitialJsonValue] = useState(initialJson); diff --git a/x-pack/packages/kbn-ai-assistant/src/hooks/use_scope.ts b/x-pack/packages/kbn-ai-assistant/src/hooks/use_scope.ts new file mode 100644 index 0000000000000..ed752e5011293 --- /dev/null +++ b/x-pack/packages/kbn-ai-assistant/src/hooks/use_scope.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 { useObservable } from 'react-use/lib'; +import { useAIAssistantAppService } from './use_ai_assistant_app_service'; + +export const useScope = () => { + const service = useAIAssistantAppService(); + const scope = useObservable(service.scope$); + return scope || 'all'; +}; diff --git a/x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts b/x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts index 460722c49be64..d7c332dc042d3 100644 --- a/x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts +++ b/x-pack/packages/kbn-ai-assistant/src/utils/create_mock_chat_service.ts @@ -7,9 +7,11 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { + FunctionDefinition, MessageRole, ObservabilityAIAssistantChatService, } from '@kbn/observability-ai-assistant-plugin/public'; +import { BehaviorSubject } from 'rxjs'; type MockedChatService = DeeplyMockedKeys; @@ -18,6 +20,7 @@ export const createMockChatService = (): MockedChatService => { chat: jest.fn(), complete: jest.fn(), sendAnalyticsEvent: jest.fn(), + functions$: new BehaviorSubject([]) as MockedChatService['functions$'], getFunctions: jest.fn().mockReturnValue([]), hasFunction: jest.fn().mockReturnValue(false), hasRenderFunction: jest.fn().mockReturnValue(true), @@ -29,6 +32,7 @@ export const createMockChatService = (): MockedChatService => { content: '', }, }), + getScope: jest.fn(), }; return mockChatService; }; diff --git a/x-pack/packages/kbn-ai-assistant/tsconfig.json b/x-pack/packages/kbn-ai-assistant/tsconfig.json index c8d91c9d37450..286bf8d5d1012 100644 --- a/x-pack/packages/kbn-ai-assistant/tsconfig.json +++ b/x-pack/packages/kbn-ai-assistant/tsconfig.json @@ -35,5 +35,6 @@ "@kbn/code-editor", "@kbn/ml-plugin", "@kbn/share-plugin", + "@kbn/ai-assistant-common", ] } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/functions/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/functions/types.ts index 8bdb76388cb56..5b93005a7fc26 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/functions/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/functions/types.ts @@ -6,9 +6,9 @@ */ import type { JSONSchema7TypeName } from 'json-schema'; import type { Observable } from 'rxjs'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { ChatCompletionChunkEvent, MessageAddEvent } from '../conversation_complete'; import { FunctionVisibility } from './function_visibility'; -import { AssistantScope } from '../types'; export { FunctionVisibility }; type JSONSchemaOrPrimitive = CompatibleJSONSchema | string | number | boolean; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts index 2c65ee6dc06b2..7595c42e4dc93 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/types.ts @@ -6,6 +6,7 @@ */ import { IconType } from '@elastic/eui'; import type { ToolSchema } from '@kbn/inference-plugin/common'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import type { ObservabilityAIAssistantChatService } from '../public'; import type { FunctionResponse } from './functions/types'; @@ -145,6 +146,7 @@ export interface StarterPrompt { title: string; prompt: string; icon: IconType; + scopes?: AssistantScope[]; } export interface ObservabilityAIAssistantScreenContext { @@ -157,5 +159,3 @@ export interface ObservabilityAIAssistantScreenContext { actions?: Array>; starterPrompts?: StarterPrompt[]; } - -export type AssistantScope = 'observability' | 'search' | 'all'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/filter_function_definitions.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/filter_function_definitions.ts index b9d41fb498056..de03d38fed663 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/filter_function_definitions.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/common/utils/filter_function_definitions.ts @@ -13,7 +13,7 @@ export function filterFunctionDefinitions({ }: { filter?: string; definitions: FunctionDefinition[]; -}) { +}): FunctionDefinition[] { return filter ? definitions.filter((fn) => { const matchesFilter = diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/insight/insight.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/insight/insight.tsx index 97dac18d4f733..08bf1414b15b5 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/insight/insight.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/insight/insight.tsx @@ -56,7 +56,7 @@ function ChatContent({ }) { const service = useObservabilityAIAssistant(); const chatService = useObservabilityAIAssistantChatService(); - const { scope } = service; + const scope = chatService.getScope(); const initialMessagesRef = useRef(initialMessages); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.test.ts index 7a88e4bd5486b..28e2a3709a355 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.test.ts @@ -6,7 +6,7 @@ */ import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import { act, renderHook, type RenderHookResult } from '@testing-library/react-hooks'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { MessageRole, type ObservabilityAIAssistantChatService, @@ -14,6 +14,7 @@ import { } from '..'; import { createInternalServerError, + FunctionDefinition, StreamingChatResponseEventType, type StreamingChatResponseEventWithoutError, } from '../../common'; @@ -26,6 +27,7 @@ const mockChatService: MockedChatService = { chat: jest.fn(), complete: jest.fn(), sendAnalyticsEvent: jest.fn(), + functions$: new BehaviorSubject([]) as MockedChatService['functions$'], getFunctions: jest.fn().mockReturnValue([]), hasFunction: jest.fn().mockReturnValue(false), hasRenderFunction: jest.fn().mockReturnValue(true), @@ -37,6 +39,7 @@ const mockChatService: MockedChatService = { role: MessageRole.System, }, }), + getScope: jest.fn(), }; const addErrorMock = jest.fn(); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts index b621b3b151713..48884664ec646 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/hooks/use_chat.ts @@ -10,7 +10,7 @@ import { merge } from 'lodash'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import type { NotificationsStart } from '@kbn/core/public'; -import { AssistantScope } from '../../common/types'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { MessageRole, type Message, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/mock.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/public/mock.tsx index 349f044206267..0731f26476da3 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/mock.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/mock.tsx @@ -7,7 +7,8 @@ import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; import React from 'react'; -import { Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { AssistantScope } from '@kbn/ai-assistant-common'; import type { ChatCompletionChunkEvent, StreamingChatResponseEventWithoutError, @@ -21,12 +22,14 @@ import type { ObservabilityAIAssistantService, } from './types'; import { buildFunctionElasticsearch, buildFunctionServiceSummary } from './utils/builders'; +import { FunctionDefinition } from '../common'; export const mockChatService: ObservabilityAIAssistantChatService = { sendAnalyticsEvent: noop, chat: (options) => new Observable(), complete: (options) => new Observable(), getFunctions: () => [buildFunctionElasticsearch(), buildFunctionServiceSummary()], + functions$: new BehaviorSubject([] as FunctionDefinition[]), renderFunction: (name) => (
{i18n.translate('xpack.observabilityAiAssistant.chatService.div.helloLabel', { @@ -44,6 +47,7 @@ export const mockChatService: ObservabilityAIAssistantChatService = { content: 'System', }, }), + getScope: jest.fn(), }; export const mockService: ObservabilityAIAssistantService = { @@ -60,7 +64,9 @@ export const mockService: ObservabilityAIAssistantService = { predefinedConversation$: new Observable(), }, navigate: async () => of(), - scope: 'all', + setScope: jest.fn(), + getScope: jest.fn(), + scope$: new BehaviorSubject('all'), }; function createSetupContract(): ObservabilityAIAssistantPublicSetup { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/plugin.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/public/plugin.tsx index 05c4bc93cd2ae..cb9cc7d941147 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/plugin.tsx @@ -10,6 +10,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import type { Logger } from '@kbn/logging'; import { withSuspense } from '@kbn/shared-ux-utility'; import React, { type ComponentType, lazy, type Ref } from 'react'; +import { AssistantScope } from '@kbn/ai-assistant-common'; import { registerTelemetryEventTypes } from './analytics'; import { ObservabilityAIAssistantChatServiceContext } from './context/observability_ai_assistant_chat_service_context'; import { ObservabilityAIAssistantMultipaneFlyoutContext } from './context/observability_ai_assistant_multipane_flyout_context'; @@ -41,16 +42,17 @@ export class ObservabilityAIAssistantPlugin { logger: Logger; service?: ObservabilityAIAssistantService; + scopeFromConfig?: AssistantScope; constructor(context: PluginInitializerContext) { this.logger = context.logger.get(); + this.scopeFromConfig = context.config.get().scope; } setup( coreSetup: CoreSetup, pluginsSetup: ObservabilityAIAssistantPluginSetupDependencies ): ObservabilityAIAssistantPublicSetup { registerTelemetryEventTypes(coreSetup.analytics); - return {}; } @@ -65,7 +67,8 @@ export class ObservabilityAIAssistantPlugin coreStart.application.capabilities.observabilityAIAssistant[ aiAssistantCapabilities.show ] === true, - scope: 'observability', + scope: this.scopeFromConfig || 'observability', + scopeIsMutable: !!this.scopeFromConfig, })); const withProviders =

( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.test.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.test.ts index c495d33301882..05e0a89c4b7ad 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.test.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.test.ts @@ -5,7 +5,7 @@ * 2.0. */ import type { HttpFetchOptions } from '@kbn/core/public'; -import { filter, lastValueFrom, Observable } from 'rxjs'; +import { BehaviorSubject, filter, lastValueFrom, Observable } from 'rxjs'; import { ReadableStream } from 'stream/web'; import { AbortError } from '@kbn/kibana-utils-plugin/common'; import { @@ -17,6 +17,7 @@ import { import { concatenateChatCompletionChunks } from '../../common/utils/concatenate_chat_completion_chunks'; import type { ObservabilityAIAssistantChatService } from '../types'; import { createChatService } from './create_chat_service'; +import { AssistantScope } from '@kbn/ai-assistant-common'; async function getConcatenatedMessage( response$: Observable @@ -70,7 +71,7 @@ describe('createChatService', () => { apiClient: clientSpy, registrations: [], signal: new AbortController().signal, - scope: 'observability', + scope$: new BehaviorSubject('observability'), }); }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.ts index e1829308fe69a..04520cb70a588 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_chat_service.ts @@ -23,7 +23,8 @@ import { throwError, timestamp, } from 'rxjs'; -import { AssistantScope } from '../../common/types'; +import { BehaviorSubject } from 'rxjs'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { ChatCompletionChunkEvent, Message, MessageRole } from '../../common'; import { StreamingChatResponseEventType, @@ -31,11 +32,15 @@ import { type StreamingChatResponseEvent, type StreamingChatResponseEventWithoutError, } from '../../common/conversation_complete'; -import { FunctionRegistry, FunctionResponse } from '../../common/functions/types'; +import { + FunctionDefinition, + FunctionRegistry, + FunctionResponse, +} from '../../common/functions/types'; import { filterFunctionDefinitions } from '../../common/utils/filter_function_definitions'; import { throwSerializedChatCompletionErrors } from '../../common/utils/throw_serialized_chat_completion_errors'; import { untilAborted } from '../../common/utils/until_aborted'; -import { sendEvent } from '../analytics'; +import { TelemetryEventTypeWithPayload, sendEvent } from '../analytics'; import type { ObservabilityAIAssistantAPIClient, ObservabilityAIAssistantAPIClientRequestParamsOf, @@ -48,6 +53,7 @@ import type { } from '../types'; import { readableStreamReaderIntoObservable } from '../utils/readable_stream_reader_into_observable'; import { complete } from './complete'; +import { ChatActionClickHandler } from '../components/chat/types'; const MIN_DELAY = 10; @@ -133,60 +139,131 @@ function serialize( ); } -export async function createChatService({ - analytics, - signal: setupAbortSignal, - registrations, - apiClient, - scope, -}: { - analytics: AnalyticsServiceStart; - signal: AbortSignal; - registrations: ChatRegistrationRenderFunction[]; - apiClient: ObservabilityAIAssistantAPIClient; - scope: AssistantScope; -}): Promise { - const functionRegistry: FunctionRegistry = new Map(); +class ChatService { + private functionRegistry: FunctionRegistry; + private renderFunctionRegistry: Map>; + private abortSignal: AbortSignal; + private apiClient: ObservabilityAIAssistantAPIClient; + public scope$: BehaviorSubject; + private analytics: AnalyticsServiceStart; + private registrations: ChatRegistrationRenderFunction[]; + private systemMessage: string; + public functions$: BehaviorSubject; - const renderFunctionRegistry: Map> = new Map(); + constructor({ + abortSignal, + apiClient, + scope$, + analytics, + registrations, + }: { + abortSignal: AbortSignal; + apiClient: ObservabilityAIAssistantAPIClient; + scope$: BehaviorSubject; + analytics: AnalyticsServiceStart; + registrations: ChatRegistrationRenderFunction[]; + }) { + this.functionRegistry = new Map(); + this.renderFunctionRegistry = new Map(); + this.abortSignal = abortSignal; + this.apiClient = apiClient; + this.scope$ = scope$; + this.analytics = analytics; + this.registrations = registrations; + this.systemMessage = ''; + this.functions$ = new BehaviorSubject([] as FunctionDefinition[]); + scope$.subscribe(() => { + this.initialize(); + }); + } - const [{ functionDefinitions, systemMessage }] = await Promise.all([ - apiClient('GET /internal/observability_ai_assistant/{scope}/functions', { - signal: setupAbortSignal, - params: { - path: { - scope, - }, - }, - }), - ...registrations.map((registration) => { - return registration({ - registerRenderFunction: (name, renderFn) => { - renderFunctionRegistry.set(name, renderFn); + private getClient = () => { + return { + chat: this.chat, + complete: this.complete, + }; + }; + + async initialize() { + this.functionRegistry = new Map(); + const [{ functionDefinitions, systemMessage }] = await Promise.all([ + this.apiClient('GET /internal/observability_ai_assistant/{scope}/functions', { + signal: this.abortSignal, + params: { + path: { + scope: this.getScope(), + }, }, - }); - }), - ]); + }), + ...this.registrations.map((registration) => { + return registration({ + registerRenderFunction: (name, renderFn) => { + this.renderFunctionRegistry.set(name, renderFn); + }, + }); + }), + ]); - functionDefinitions.forEach((fn) => { - functionRegistry.set(fn.name, fn); - }); + functionDefinitions.forEach((fn) => { + this.functionRegistry.set(fn.name, fn); + }); + this.systemMessage = systemMessage; + + this.functions$.next(this.getFunctions()); + } + + public sendAnalyticsEvent = (event: TelemetryEventTypeWithPayload) => { + sendEvent(this.analytics, event); + }; + + public renderFunction = ( + name: string, + args: string | undefined, + response: { data?: string; content?: string }, + onActionClick: ChatActionClickHandler + ) => { + const fn = this.renderFunctionRegistry.get(name); - const getFunctions = (options?: { contexts?: string[]; filter?: string }) => { + if (!fn) { + throw new Error(`Function ${name} not found`); + } + + const parsedArguments = args ? JSON.parse(args) : {}; + + const parsedResponse = { + content: JSON.parse(response.content ?? '{}'), + data: JSON.parse(response.data ?? '{}'), + }; + + return fn?.({ + response: parsedResponse, + arguments: parsedArguments, + onActionClick, + }); + }; + + public getFunctions = (options?: { + contexts?: string[]; + filter?: string; + }): FunctionDefinition[] => { return filterFunctionDefinitions({ ...options, - definitions: functionDefinitions, + definitions: Array.from(this.functionRegistry.values()), + }).filter((value) => { + return value.scopes + ? value.scopes?.includes(this.getScope()) || value.scopes?.includes('all') + : true; }); }; - function callStreamingApi( + public callStreamingApi( endpoint: TEndpoint, options: { signal: AbortSignal; } & ObservabilityAIAssistantAPIClientRequestParamsOf ): Observable { return from( - apiClient(endpoint, { + this.apiClient(endpoint, { ...options, asResponse: true, rawResponse: true, @@ -194,101 +271,103 @@ export async function createChatService({ ).pipe(serialize(options.signal)); } - const client: Pick = { - chat(name: string, { connectorId, messages, functionCall, functions, signal }) { - return callStreamingApi('POST /internal/observability_ai_assistant/chat', { - params: { - body: { - name, - messages, - connectorId, - functionCall, - functions: functions ?? [], - scope, - }, - }, - signal, - }).pipe( - filter( - (line): line is ChatCompletionChunkEvent => - line.type === StreamingChatResponseEventType.ChatCompletionChunk - ) - ); - }, - complete({ - getScreenContexts, - connectorId, - conversationId, - messages, - persist, - disableFunctions, - signal, + public hasFunction = (name: string) => { + return this.functionRegistry.has(name); + }; - instructions, - }) { - return complete( - { - getScreenContexts, - connectorId, - conversationId, + public hasRenderFunction = (name: string) => { + return this.renderFunctionRegistry.has(name); + }; + + public getSystemMessage = (): Message => { + return { + '@timestamp': new Date().toISOString(), + message: { + role: MessageRole.System, + content: this.systemMessage, + }, + }; + }; + + public chat: ObservabilityAIAssistantChatService['chat'] = ( + name: string, + { connectorId, messages, functionCall, functions, signal } + ) => { + return this.callStreamingApi('POST /internal/observability_ai_assistant/chat', { + params: { + body: { + name, messages, - persist, - disableFunctions, - signal, - client, - instructions, - scope, + connectorId, + functionCall, + functions: functions ?? [], + scope: this.getScope(), }, - ({ params }) => { - return callStreamingApi('POST /internal/observability_ai_assistant/chat/complete', { - params, - signal, - }); - } - ); - }, + }, + signal, + }).pipe( + filter( + (line): line is ChatCompletionChunkEvent => + line.type === StreamingChatResponseEventType.ChatCompletionChunk + ) + ); }; - return { - sendAnalyticsEvent: (event) => { - sendEvent(analytics, event); - }, - renderFunction: (name, args, response, onActionClick) => { - const fn = renderFunctionRegistry.get(name); - - if (!fn) { - throw new Error(`Function ${name} not found`); + public complete: ObservabilityAIAssistantChatService['complete'] = ({ + getScreenContexts, + connectorId, + conversationId, + messages, + persist, + disableFunctions, + signal, + instructions, + }) => { + return complete( + { + getScreenContexts, + connectorId, + conversationId, + messages, + persist, + disableFunctions, + signal, + client: this.getClient(), + instructions, + scope: this.getScope(), + }, + ({ params }) => { + return this.callStreamingApi('POST /internal/observability_ai_assistant/chat/complete', { + params, + signal, + }); } + ); + }; - const parsedArguments = args ? JSON.parse(args) : {}; - - const parsedResponse = { - content: JSON.parse(response.content ?? '{}'), - data: JSON.parse(response.data ?? '{}'), - }; + public getScope() { + return this.scope$.value; + } +} - return fn?.({ - response: parsedResponse, - arguments: parsedArguments, - onActionClick, - }); - }, - getFunctions, - hasFunction: (name: string) => { - return functionRegistry.has(name); - }, - hasRenderFunction: (name: string) => { - return renderFunctionRegistry.has(name); - }, - getSystemMessage: (): Message => { - return { - '@timestamp': new Date().toISOString(), - message: { - role: MessageRole.System, - content: systemMessage, - }, - }; - }, - ...client, - }; +export async function createChatService({ + analytics, + signal: setupAbortSignal, + registrations, + apiClient, + scope$, +}: { + analytics: AnalyticsServiceStart; + signal: AbortSignal; + registrations: ChatRegistrationRenderFunction[]; + apiClient: ObservabilityAIAssistantAPIClient; + scope$: BehaviorSubject; +}): Promise { + return new ChatService({ + analytics, + apiClient, + scope$, + registrations, + abortSignal: setupAbortSignal, + }); } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_mock_chat_service.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_mock_chat_service.ts index fc26499b1f63e..9af669242e436 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_mock_chat_service.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_mock_chat_service.ts @@ -6,7 +6,8 @@ */ import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import { MessageRole } from '../../common'; +import { BehaviorSubject } from 'rxjs'; +import { FunctionDefinition, MessageRole } from '../../common'; import type { ObservabilityAIAssistantChatService } from '../types'; type MockedChatService = DeeplyMockedKeys; @@ -16,6 +17,7 @@ export const createMockChatService = (): MockedChatService => { chat: jest.fn(), complete: jest.fn(), sendAnalyticsEvent: jest.fn(), + functions$: new BehaviorSubject([]) as MockedChatService['functions$'], getFunctions: jest.fn().mockReturnValue([]), hasFunction: jest.fn().mockReturnValue(false), hasRenderFunction: jest.fn().mockReturnValue(true), @@ -27,6 +29,7 @@ export const createMockChatService = (): MockedChatService => { content: 'system', }, }), + getScope: jest.fn(), }; return mockChatService; }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_service.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_service.ts index ae95cb26148e9..22d1b9c792f7f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_service.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/create_service.ts @@ -8,11 +8,8 @@ import type { AnalyticsServiceStart, CoreStart } from '@kbn/core/public'; import { compact, without } from 'lodash'; import { BehaviorSubject, debounceTime, filter, lastValueFrom, of, Subject, take } from 'rxjs'; -import type { - AssistantScope, - Message, - ObservabilityAIAssistantScreenContext, -} from '../../common/types'; +import { type AssistantScope, filterScopes } from '@kbn/ai-assistant-common'; +import type { Message, ObservabilityAIAssistantScreenContext } from '../../common/types'; import { createFunctionRequestMessage } from '../../common/utils/create_function_request_message'; import { createFunctionResponseMessage } from '../../common/utils/create_function_response_message'; import { createCallObservabilityAIAssistantAPI } from '../api'; @@ -24,11 +21,13 @@ export function createService({ coreStart, enabled, scope, + scopeIsMutable, }: { analytics: AnalyticsServiceStart; coreStart: CoreStart; enabled: boolean; scope: AssistantScope; + scopeIsMutable: boolean; }): ObservabilityAIAssistantService { const apiClient = createCallObservabilityAIAssistantAPI(coreStart); @@ -39,6 +38,17 @@ export function createService({ ]); const predefinedConversation$ = new Subject<{ messages: Message[]; title?: string }>(); + const scope$ = new BehaviorSubject(scope); + + const getScreenContexts = () => { + const currentScope = scope$.value; + const screenContexts = screenContexts$.value.map(({ starterPrompts, ...rest }) => ({ + ...rest, + starterPrompts: starterPrompts?.filter(filterScopes(currentScope)), + })); + return screenContexts; + }; + return { isEnabled: () => { return enabled; @@ -48,12 +58,10 @@ export function createService({ }, start: async ({ signal }) => { const mod = await import('./create_chat_service'); - return await mod.createChatService({ analytics, apiClient, signal, registrations, scope }); + return await mod.createChatService({ analytics, apiClient, signal, registrations, scope$ }); }, callApi: apiClient, - getScreenContexts() { - return screenContexts$.value; - }, + getScreenContexts, setScreenContext: (context: ObservabilityAIAssistantScreenContext) => { screenContexts$.next(screenContexts$.value.concat(context)); @@ -83,7 +91,7 @@ export function createService({ name: 'context', content: { screenDescription: compact( - screenContexts$.value.map((context) => context.screenDescription) + getScreenContexts().map((context) => context.screenDescription) ).join('\n\n'), }, }) @@ -95,6 +103,12 @@ export function createService({ }, predefinedConversation$: predefinedConversation$.asObservable(), }, - scope, + setScope: (newScope: AssistantScope) => { + if (!scopeIsMutable) { + scope$.next(newScope); + } + }, + getScope: () => scope$.value, + scope$, }; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/default_starter_prompts.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/default_starter_prompts.ts index a7d460f6eaf47..4811054da3ed7 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/default_starter_prompts.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/service/default_starter_prompts.ts @@ -6,8 +6,9 @@ */ import { i18n } from '@kbn/i18n'; +import { StarterPrompt } from '../../common/types'; -export const defaultStarterPrompts = [ +export const defaultStarterPrompts: StarterPrompt[] = [ { title: i18n.translate( 'xpack.observabilityAiAssistant.app.starterPrompts.exampleQuestions.title', @@ -20,6 +21,7 @@ export const defaultStarterPrompts = [ } ), icon: 'sparkles', + scopes: ['all'], }, { title: i18n.translate( @@ -33,6 +35,7 @@ export const defaultStarterPrompts = [ } ), icon: 'inspect', + scopes: ['all'], }, { title: i18n.translate('xpack.observabilityAiAssistant.app.starterPrompts.doIHaveAlerts.title', { @@ -45,6 +48,7 @@ export const defaultStarterPrompts = [ } ), icon: 'bell', + scopes: ['observability'], }, { title: i18n.translate('xpack.observabilityAiAssistant.app.starterPrompts.whatAreSlos.title', { @@ -54,5 +58,6 @@ export const defaultStarterPrompts = [ defaultMessage: 'What are SLOs?', }), icon: 'bullseye', + scopes: ['observability'], }, ]; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/storybook_mock.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/public/storybook_mock.tsx index f48d7868def87..19d51bf4c66d1 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/storybook_mock.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/storybook_mock.tsx @@ -7,8 +7,9 @@ import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; import React from 'react'; -import { Observable, of } from 'rxjs'; -import { ChatCompletionChunkEvent, MessageRole } from '.'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { AssistantScope } from '@kbn/ai-assistant-common'; +import { ChatCompletionChunkEvent, FunctionDefinition, MessageRole } from '.'; import type { StreamingChatResponseEventWithoutError } from '../common/conversation_complete'; import type { ObservabilityAIAssistantAPIClient } from './api'; import type { ObservabilityAIAssistantChatService, ObservabilityAIAssistantService } from './types'; @@ -36,6 +37,10 @@ export const createStorybookChatService = (): ObservabilityAIAssistantChatServic content: 'System', }, }), + functions$: new BehaviorSubject( + [] + ) as ObservabilityAIAssistantChatService['functions$'], + getScope: () => 'all', }); export const createStorybookService = (): ObservabilityAIAssistantService => ({ @@ -52,5 +57,7 @@ export const createStorybookService = (): ObservabilityAIAssistantService => ({ predefinedConversation$: new Observable(), }, navigate: async () => of(), - scope: 'observability', + scope$: new BehaviorSubject('all') as ObservabilityAIAssistantService['scope$'], + getScope: () => 'all', + setScope: () => {}, }); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/public/types.ts index 8b265d433f515..b13d81faa3a3b 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/types.ts @@ -8,6 +8,8 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import type { Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import type { ChatCompletionChunkEvent, MessageAddEvent, @@ -19,7 +21,6 @@ import type { ObservabilityAIAssistantScreenContext, PendingMessage, AdHocInstruction, - AssistantScope, } from '../common/types'; import type { TelemetryEventTypeWithPayload } from './analytics'; import type { ObservabilityAIAssistantAPIClient } from './api'; @@ -76,6 +77,7 @@ export interface ObservabilityAIAssistantChatService { filter?: string; scope: AssistantScope; }) => FunctionDefinition[]; + functions$: BehaviorSubject; hasFunction: (name: string) => boolean; getSystemMessage: () => Message; hasRenderFunction: (name: string) => boolean; @@ -83,9 +85,9 @@ export interface ObservabilityAIAssistantChatService { name: string, args: string | undefined, response: { data?: string; content?: string }, - onActionClick: ChatActionClickHandler, - scope?: AssistantScope + onActionClick: ChatActionClickHandler ) => React.ReactNode; + getScope: () => AssistantScope; } export interface ObservabilityAIAssistantConversationService { @@ -102,7 +104,9 @@ export interface ObservabilityAIAssistantService { getScreenContexts: () => ObservabilityAIAssistantScreenContext[]; conversations: ObservabilityAIAssistantConversationService; navigate: (callback: () => void) => Promise>; - scope: AssistantScope; + scope$: BehaviorSubject; + setScope: (scope: AssistantScope) => void; + getScope: () => AssistantScope; } export type RenderFunction = (options: { @@ -120,7 +124,9 @@ export type ChatRegistrationRenderFunction = ({}: { registerRenderFunction: RegisterRenderFunctionDefinition; }) => Promise; -export interface ConfigSchema {} +export interface ConfigSchema { + scope?: AssistantScope; +} export interface ObservabilityAIAssistantPluginSetupDependencies { licensing: {}; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts index abb4590c89696..dc9a780c82a1f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/config.ts @@ -10,6 +10,7 @@ import { schema, type TypeOf } from '@kbn/config-schema'; export const config = schema.object({ enabled: schema.boolean({ defaultValue: true }), modelId: schema.maybe(schema.string()), + scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])), }); export type ObservabilityAIAssistantConfig = TypeOf; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/index.ts index 7d1409b29dde3..0ad41969cedc2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/index.ts @@ -47,7 +47,7 @@ export const config: PluginConfigDescriptor = { level: 'warning', }), ], - exposeToBrowser: {}, + exposeToBrowser: { scope: true }, schema: configSchema, }; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts index f5f235af12544..136cc68497563 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/chat/route.ts @@ -10,7 +10,7 @@ import { context as otelContext } from '@opentelemetry/api'; import * as t from 'io-ts'; import { from, map } from 'rxjs'; import { Readable } from 'stream'; -import { AssistantScope } from '../../../common/types'; +import { AssistantScope } from '@kbn/ai-assistant-common'; import { aiAssistantSimulatedFunctionCalling } from '../..'; import { createFunctionResponseMessage } from '../../../common/utils/create_function_response_message'; import { withoutTokenCountEvents } from '../../../common/utils/without_token_count_events'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts index 1506db576275d..b31e33148454c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/functions/route.ts @@ -61,7 +61,7 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({ const availableFunctionNames = functionDefinitions.map((def) => def.name); return { - functionDefinitions: functionClient.getFunctions().map((fn) => fn.definition), + functionDefinitions, systemMessage: getSystemMessageFromInstructions({ applicationInstructions: functionClient.getInstructions(scope), userInstructions, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/runtime_types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/runtime_types.ts index 3ead874f22ca1..68f91f7e09858 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/runtime_types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/routes/runtime_types.ts @@ -134,11 +134,14 @@ export const functionRt = t.intersection([ }), ]); -export const starterPromptRt: t.Type = t.type({ - title: t.string, - prompt: t.string, - icon: t.any, -}); +export const starterPromptRt: t.Type = t.intersection([ + t.type({ + title: t.string, + prompt: t.string, + icon: t.any, + }), + t.partial({ scopes: t.array(assistantScopeType) }), +]); export const screenContextRt: t.Type = t.partial({ description: t.string, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/chat_function_client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/chat_function_client/index.ts index d8fbe456da879..4413e4fa8b634 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/chat_function_client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/chat_function_client/index.ts @@ -9,12 +9,9 @@ import Ajv, { type ErrorObject, type ValidateFunction } from 'ajv'; import dedent from 'dedent'; import { compact, keyBy } from 'lodash'; +import { type AssistantScope, filterScopes } from '@kbn/ai-assistant-common'; import { FunctionVisibility, type FunctionResponse } from '../../../common/functions/types'; -import type { - AssistantScope, - Message, - ObservabilityAIAssistantScreenContextRequest, -} from '../../../common/types'; +import type { Message, ObservabilityAIAssistantScreenContextRequest } from '../../../common/types'; import { filterFunctionDefinitions } from '../../../common/utils/filter_function_definitions'; import type { FunctionCallChatFunction, @@ -114,11 +111,7 @@ export class ChatFunctionClient { } getInstructions(scope: AssistantScope): InstructionOrCallback[] { - return this.instructions - .filter( - (instruction) => instruction.scopes.includes(scope) || instruction.scopes.includes('all') - ) - .map((i) => i.instruction); + return this.instructions.filter(filterScopes(scope)).map((i) => i.instruction); } hasAction(name: string) { @@ -133,9 +126,7 @@ export class ChatFunctionClient { scope?: AssistantScope; } = {}): FunctionHandler[] { const allFunctions = Array.from(this.functionRegistry.values()) - .filter(({ handler, scopes }) => - scope ? scopes.includes(scope) || scopes.includes('all') : true - ) + .filter(filterScopes(scope)) .map(({ handler }) => handler); const functionsByName = keyBy(allFunctions, (definition) => definition.definition.name); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts index fc14558776434..4eb0e54f9febe 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/index.ts @@ -30,6 +30,7 @@ import { } from 'rxjs'; import { Readable } from 'stream'; import { v4 } from 'uuid'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { resourceNames } from '..'; import { ObservabilityAIAssistantConnectorType } from '../../../common/connectors'; import { @@ -52,7 +53,6 @@ import { type KnowledgeBaseEntry, type Message, type AdHocInstruction, - AssistantScope, } from '../../../common/types'; import { withoutTokenCountEvents } from '../../../common/utils/without_token_count_events'; import { CONTEXT_FUNCTION_NAME } from '../../functions/context'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/operators/continue_conversation.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/operators/continue_conversation.ts index b91600323d41e..7ebd9d66bf30f 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/operators/continue_conversation.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/client/operators/continue_conversation.ts @@ -21,6 +21,7 @@ import { switchMap, throwError, } from 'rxjs'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { CONTEXT_FUNCTION_NAME } from '../../../functions/context'; import { createFunctionNotFoundError, Message, MessageRole } from '../../../../common'; import { @@ -28,7 +29,7 @@ import { MessageOrChatEvent, } from '../../../../common/conversation_complete'; import { FunctionVisibility } from '../../../../common/functions/types'; -import { AdHocInstruction, AssistantScope, Instruction } from '../../../../common/types'; +import { AdHocInstruction, Instruction } from '../../../../common/types'; import { createFunctionResponseMessage } from '../../../../common/utils/create_function_response_message'; import { emitWithConcatenatedMessage } from '../../../../common/utils/emit_with_concatenated_message'; import { withoutTokenCountEvents } from '../../../../common/utils/without_token_count_events'; @@ -137,6 +138,7 @@ function getFunctionDefinitions({ functionClient, functionLimitExceeded, disableFunctions, + scope, }: { functionClient: ChatFunctionClient; functionLimitExceeded: boolean; @@ -145,13 +147,14 @@ function getFunctionDefinitions({ | { except: string[]; }; + scope: AssistantScope; }) { if (functionLimitExceeded || disableFunctions === true) { return []; } let systemFunctions = functionClient - .getFunctions() + .getFunctions({ scope }) .map((fn) => fn.definition) .filter( (def) => @@ -213,6 +216,7 @@ export function continueConversation({ functionLimitExceeded, functionClient, disableFunctions, + scope, }); const messagesWithUpdatedSystemMessage = replaceSystemMessage( diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index 359692809f3a4..f203dcc350bfd 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -12,8 +12,8 @@ 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 type { AssistantScope } from '@kbn/ai-assistant-common'; import { - AssistantScope, KnowledgeBaseEntryRole, ObservabilityAIAssistantScreenContextRequest, } from '../../common/types'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts index 66510008df967..4857189f2d156 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/types.ts @@ -7,6 +7,7 @@ import type { FromSchema } from 'json-schema-to-ts'; import { Observable } from 'rxjs'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { ChatCompletionChunkEvent, ChatEvent } from '../../common/conversation_complete'; import type { CompatibleJSONSchema, @@ -17,7 +18,6 @@ import type { Message, ObservabilityAIAssistantScreenContextRequest, InstructionOrPlainText, - AssistantScope, } from '../../common/types'; import type { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types'; import { ChatFunctionClient } from './chat_function_client'; diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json index d9c747731073b..63105b2a86c59 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json @@ -45,7 +45,8 @@ "@kbn/core-elasticsearch-server", "@kbn/core-ui-settings-server", "@kbn/inference-plugin", - "@kbn/management-settings-ids" + "@kbn/management-settings-ids", + "@kbn/ai-assistant-common" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx index 2c2af65accb59..01202b385917a 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/components/nav_control/index.tsx @@ -18,6 +18,7 @@ import { useTheme } from '../../hooks/use_theme'; import { useNavControlScreenContext } from '../../hooks/use_nav_control_screen_context'; import { SharedProviders } from '../../utils/shared_providers'; import { ObservabilityAIAssistantAppPluginStartDependencies } from '../../types'; +import { useNavControlScope } from '../../hooks/use_nav_control_scope'; interface NavControlWithProviderDeps { appService: AIAssistantAppService; @@ -61,6 +62,7 @@ export function NavControl() { const [hasBeenOpened, setHasBeenOpened] = useState(false); useNavControlScreenContext(); + useNavControlScope(); const chatService = useAbortableAsync( ({ signal }) => { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx index f82de790e05c2..429e39f1a177c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx @@ -32,7 +32,10 @@ function getVisibility( return categoryId !== DEFAULT_APP_CATEGORIES.security.id; } - return categoryId === DEFAULT_APP_CATEGORIES.observability.id; + return [ + DEFAULT_APP_CATEGORIES.observability.id, + DEFAULT_APP_CATEGORIES.enterpriseSearch.id, + ].includes(categoryId); } export function useIsNavControlVisible({ coreStart, pluginsStart }: UseIsNavControlVisibleProps) { diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_scope.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_scope.ts new file mode 100644 index 0000000000000..39080adc47d48 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/public/hooks/use_nav_control_scope.ts @@ -0,0 +1,41 @@ +/* + * 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 } from 'react'; +import { useAIAssistantAppService } from '@kbn/ai-assistant'; +import { AssistantScope } from '@kbn/ai-assistant-common'; +import { useObservable } from 'react-use/lib'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public'; +import { useKibana } from './use_kibana'; + +const scopeUrlLookup: Record = { + [DEFAULT_APP_CATEGORIES.observability.id]: 'observability', + [DEFAULT_APP_CATEGORIES.enterpriseSearch.id]: 'search', +}; + +export function useNavControlScope() { + const service = useAIAssistantAppService(); + + const { + services: { application }, + } = useKibana(); + + const currentApplication = useObservable(application.currentAppId$); + const applications = useObservable(application.applications$); + + useEffect(() => { + const currentCategoryId = + (currentApplication && applications?.get(currentApplication)?.category?.id) || + DEFAULT_APP_CATEGORIES.kibana.id; + const newScope = Object.entries(scopeUrlLookup).find( + ([categoryId]) => categoryId === currentCategoryId + )?.[1]; + if (newScope && newScope !== service.getScope()) { + service.setScope(newScope); + } + }, [applications, currentApplication, service]); +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/kibana_client.ts b/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/kibana_client.ts index ddfa4c98765f7..cc1500168e368 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/kibana_client.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/scripts/evaluation/kibana_client.ts @@ -18,10 +18,8 @@ import { StreamingChatResponseEvent, StreamingChatResponseEventType, } from '@kbn/observability-ai-assistant-plugin/common'; -import type { - AssistantScope, - ObservabilityAIAssistantScreenContext, -} from '@kbn/observability-ai-assistant-plugin/common/types'; +import type { ObservabilityAIAssistantScreenContext } from '@kbn/observability-ai-assistant-plugin/common/types'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { throwSerializedChatCompletionErrors } from '@kbn/observability-ai-assistant-plugin/common/utils/throw_serialized_chat_completion_errors'; import { isSupportedConnectorType, diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json index f5b6d1db53885..af04a677f5e94 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant_app/tsconfig.json @@ -68,6 +68,7 @@ "@kbn/task-manager-plugin", "@kbn/cloud-plugin", "@kbn/logs-data-access-plugin", + "@kbn/ai-assistant-common", ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/search_assistant/kibana.jsonc b/x-pack/plugins/search_assistant/kibana.jsonc index 8391ee14e0d88..53af40cee6cc6 100644 --- a/x-pack/plugins/search_assistant/kibana.jsonc +++ b/x-pack/plugins/search_assistant/kibana.jsonc @@ -21,6 +21,7 @@ ], "optionalPlugins": [ "cloud", + "serverless", "usageCollection", ], "requiredBundles": [ diff --git a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx index 545ff1ceb7370..f0e4a61895f39 100644 --- a/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx +++ b/x-pack/plugins/search_assistant/public/components/routes/conversations/conversation_view_with_props.tsx @@ -30,6 +30,7 @@ export function ConversationViewWithProps() { getConversationHref={(id: string) => http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || '' } + scope="search" /> ); } diff --git a/x-pack/plugins/search_assistant/server/functions/index.ts b/x-pack/plugins/search_assistant/server/functions/index.ts new file mode 100644 index 0000000000000..d1eef69615a61 --- /dev/null +++ b/x-pack/plugins/search_assistant/server/functions/index.ts @@ -0,0 +1,36 @@ +/* + * 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 { RegistrationCallback } from '@kbn/observability-ai-assistant-plugin/server'; + +export const registerFunctions: (isServerless: boolean) => RegistrationCallback = + (isServerless: boolean) => + async ({ client, functions, resources, signal }) => { + functions.registerInstruction({ + instruction: `You are a helpful assistant for Elasticsearch. Your goal is to help Elasticsearch users accomplish tasks using Kibana and Elasticsearch. You can help them construct queries, index data, search data, use Elasticsearch APIs, generate sample data, visualise and analyze data. + + It's very important to not assume what the user means. Ask them for clarification if needed. + + If you are unsure about which function should be used and with what arguments, ask the user for clarification or confirmation. + + In KQL ("kqlFilter")) escaping happens with double quotes, not single quotes. Some characters that need escaping are: ':()\\\ + /\". Always put a field value in double quotes. Best: service.name:\"opbeans-go\". Wrong: service.name:opbeans-go. This is very important! + + You can use Github-flavored Markdown in your responses. If a function returns an array, consider using a Markdown table to format the response. + + Note that the Elasticsearch query DSL is the preferred language. Do not use ES|QL. + + If you want to call a function or tool, only call it a single time per message. Wait until the function has been executed and its results + returned to you, before executing the same tool or another tool again if needed. + + The user is able to change the language which they want you to reply in on the settings page of the AI Assistant for Observability, which can be found in the ${ + isServerless ? `Project settings.` : `Stack Management app under the option AI Assistants` + }. + If the user asks how to change the language, reply in the same language the user asked in.`, + scopes: ['search'], + }); + }; diff --git a/x-pack/plugins/search_assistant/server/index.ts b/x-pack/plugins/search_assistant/server/index.ts index 027b51cfdd7d7..837dc73c4082e 100644 --- a/x-pack/plugins/search_assistant/server/index.ts +++ b/x-pack/plugins/search_assistant/server/index.ts @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { PluginInitializerContext } from '@kbn/core/server'; import { SearchAssistantPlugin } from './plugin'; export { config } from './config'; -export function plugin() { - return new SearchAssistantPlugin(); -} +export const plugin = (initializerContext: PluginInitializerContext) => + new SearchAssistantPlugin(initializerContext); export type { SearchAssistantPluginSetup, SearchAssistantPluginStart } from './types'; diff --git a/x-pack/plugins/search_assistant/server/plugin.ts b/x-pack/plugins/search_assistant/server/plugin.ts index cdd6c3ea115b2..55bf3e5b49762 100644 --- a/x-pack/plugins/search_assistant/server/plugin.ts +++ b/x-pack/plugins/search_assistant/server/plugin.ts @@ -5,20 +5,37 @@ * 2.0. */ -import type { Plugin } from '@kbn/core/server'; +import type { CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server'; -import type { SearchAssistantPluginSetup, SearchAssistantPluginStart } from './types'; +import type { + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + SearchAssistantPluginStartDependencies, +} from './types'; + +import { registerFunctions } from './functions'; export class SearchAssistantPlugin - implements Plugin + implements + Plugin< + SearchAssistantPluginSetup, + SearchAssistantPluginStart, + {}, + SearchAssistantPluginStartDependencies + > { - constructor() {} + isServerless: boolean; + + constructor(context: PluginInitializerContext) { + this.isServerless = context.env.packageInfo.buildFlavor === 'serverless'; + } public setup() { return {}; } - public start() { + public start(coreStart: CoreStart, pluginsStart: SearchAssistantPluginStartDependencies) { + pluginsStart.observabilityAIAssistant.service.register(registerFunctions(this.isServerless)); return {}; } diff --git a/x-pack/plugins/search_assistant/server/types.ts b/x-pack/plugins/search_assistant/server/types.ts index 03c2e5a46f91d..2e4c3a5588bca 100644 --- a/x-pack/plugins/search_assistant/server/types.ts +++ b/x-pack/plugins/search_assistant/server/types.ts @@ -5,7 +5,15 @@ * 2.0. */ +import { ObservabilityAIAssistantServerStart } from '@kbn/observability-ai-assistant-plugin/server'; +import { ServerlessPluginStart } from '@kbn/serverless/server'; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchAssistantPluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SearchAssistantPluginStart {} + +export interface SearchAssistantPluginStartDependencies { + observabilityAIAssistant: ObservabilityAIAssistantServerStart; + serverless?: ServerlessPluginStart; +} diff --git a/x-pack/plugins/search_assistant/tsconfig.json b/x-pack/plugins/search_assistant/tsconfig.json index d865d2bdbff83..b95020aca1dfc 100644 --- a/x-pack/plugins/search_assistant/tsconfig.json +++ b/x-pack/plugins/search_assistant/tsconfig.json @@ -22,7 +22,8 @@ "@kbn/config-schema", "@kbn/ai-assistant", "@kbn/i18n", - "@kbn/shared-ux-router" + "@kbn/shared-ux-router", + "@kbn/serverless" ], "exclude": [ "target/**/*", diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts index 552b779d2c0aa..dadf270f0df41 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/complete/functions/helpers.ts @@ -12,7 +12,7 @@ import { StreamingChatResponseEvent, } from '@kbn/observability-ai-assistant-plugin/common'; import { Readable } from 'stream'; -import { AssistantScope } from '@kbn/observability-ai-assistant-plugin/common/types'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { CreateTest } from '../../../common/config'; function decodeEvents(body: Readable | string) { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 038e9baa5086a..03fbddf161f00 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -184,6 +184,7 @@ "@kbn/mock-idp-utils", "@kbn/cloud-security-posture-common", "@kbn/saved-objects-management-plugin", - "@kbn/alerting-types" + "@kbn/alerting-types", + "@kbn/ai-assistant-common" ] } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/complete/functions/helpers.ts b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/complete/functions/helpers.ts index b9e1ec0865013..857fa71aac9e6 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/complete/functions/helpers.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/ai_assistant/tests/complete/functions/helpers.ts @@ -11,7 +11,7 @@ import { MessageRole, StreamingChatResponseEvent, } from '@kbn/observability-ai-assistant-plugin/common'; -import { AssistantScope } from '@kbn/observability-ai-assistant-plugin/common/types'; +import type { AssistantScope } from '@kbn/ai-assistant-common'; import { Readable } from 'stream'; import type { InternalRequestHeader, RoleCredentials } from '../../../../../../../shared/services'; import { ObservabilityAIAssistantApiClient } from '../../../common/observability_ai_assistant_api_client'; diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 12ec49106c944..d61c5c19a63db 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -99,5 +99,6 @@ "@kbn/cloud-security-posture-common", "@kbn/security-plugin-types-common", "@kbn/core-saved-objects-import-export-server-internal", + "@kbn/ai-assistant-common", ] } diff --git a/yarn.lock b/yarn.lock index ae6b9dedf0400..cc938559a1947 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3311,6 +3311,10 @@ version "0.0.0" uid "" +"@kbn/ai-assistant-common@link:x-pack/packages/kbn-ai-assistant-common": + version "0.0.0" + uid "" + "@kbn/ai-assistant-management-plugin@link:src/plugins/ai_assistant_management/selection": version "0.0.0" uid ""