[8.x] [Search Assistant] Use scopes to modify behavior contextually (#195785) (#196014)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Search Assistant] Use scopes to modify behavior contextually
(#195785)](https://github.com/elastic/kibana/pull/195785)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Sander
Philipse","email":"94373878+sphilipse@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-11T23:09:06Z","message":"[Search
Assistant] Use scopes to modify behavior contextually (#195785)\n\n##
Summary\r\n\r\nThis actually uses the Search Assistant scope to modify
the assistant's\r\nbehavior depending on the context they're in. The
assistant now:\r\n- Defaults to Observability mode\r\n- Is a Search
assistant in the Search pages\r\n- Switches dynamically, changing
available functions, prompts and\r\ninstructions based on
context\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"ee341d5f801ca42ed26acf0544b0bc59948d0214","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:Search","Team:Obs
AI
Assistant","ci:project-deploy-observability","v8.16.0","backport:version"],"title":"[Search
Assistant] Use scopes to modify behavior
contextually","number":195785,"url":"https://github.com/elastic/kibana/pull/195785","mergeCommit":{"message":"[Search
Assistant] Use scopes to modify behavior contextually (#195785)\n\n##
Summary\r\n\r\nThis actually uses the Search Assistant scope to modify
the assistant's\r\nbehavior depending on the context they're in. The
assistant now:\r\n- Defaults to Observability mode\r\n- Is a Search
assistant in the Search pages\r\n- Switches dynamically, changing
available functions, prompts and\r\ninstructions based on
context\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"ee341d5f801ca42ed26acf0544b0bc59948d0214"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195785","number":195785,"mergeCommit":{"message":"[Search
Assistant] Use scopes to modify behavior contextually (#195785)\n\n##
Summary\r\n\r\nThis actually uses the Search Assistant scope to modify
the assistant's\r\nbehavior depending on the context they're in. The
assistant now:\r\n- Defaults to Observability mode\r\n- Is a Search
assistant in the Search pages\r\n- Switches dynamically, changing
available functions, prompts and\r\ninstructions based on
context\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"ee341d5f801ca42ed26acf0544b0bc59948d0214"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/196013","number":196013,"state":"OPEN"}]}]
BACKPORT-->

Co-authored-by: Sander Philipse <94373878+sphilipse@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-10-13 02:42:43 +11:00 committed by GitHub
parent d301b8f7b0
commit 4951ab959c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 628 additions and 223 deletions

1
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -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",

View file

@ -348,6 +348,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?)',
/**

View file

@ -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"],

View file

@ -0,0 +1,3 @@
# @kbn/ai-assistant-common
Provides types and utils to render the AI Assistant in plugins.

View file

@ -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';

View file

@ -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:
'<rootDir>/target/kibana-coverage/jest/x-pack/packages/kbn_ai_assistant_common_src',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/x-pack/packages/kbn-ai-assistant-common/src/**/*.{ts,tsx}',
'!<rootDir>/x-pack/packages/kbn-ai-assistant-common/src/*.test.{ts,tsx}',
],
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/packages/kbn-ai-assistant-common'],
};

View file

@ -0,0 +1,5 @@
{
"id": "@kbn/ai-assistant-common",
"owner": "@elastic/search-kibana",
"type": "shared-common"
}

View file

@ -0,0 +1,7 @@
{
"name": "@kbn/ai-assistant-common",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0",
"sideEffects": false
}

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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<T extends { scopes?: AssistantScope[] }>(scope?: AssistantScope) {
return function (value: T): boolean {
if (!scope || !value) {
return true;
}
return value?.scopes ? value.scopes.includes(scope) || value.scopes.includes('all') : true;
};
}

View file

@ -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';

View file

@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
]
}

View file

@ -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<EuiSelectableOption<FunctionListOption>>

View file

@ -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) {

View file

@ -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<ConversationViewProps> = ({
@ -33,6 +35,7 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
navigateToConversation,
getConversationHref,
newConversationHref,
scope,
}) => {
const { euiTheme } = useEuiTheme();
@ -57,6 +60,12 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
[service]
);
useEffect(() => {
if (scope) {
service.setScope(scope);
}
}, [scope, service]);
const { key: bodyKey, updateConversationIdInPlace } = useConversationKey(conversationId);
const [secondSlotContainer, setSecondSlotContainer] = useState<HTMLDivElement | null>(null);

View file

@ -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';

View file

@ -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<UseConversationProps, UseConversationResult>;
@ -54,7 +55,9 @@ const mockService: MockedService = {
predefinedConversation$: new Observable(),
},
navigate: jest.fn().mockReturnValue(of()),
scope: 'all',
scope$: new BehaviorSubject<AssistantScope>('all') as MockedService['scope$'],
setScope: jest.fn(),
getScope: jest.fn(),
};
const mockChatService = createMockChatService();

View file

@ -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: {

View file

@ -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 || [];
};

View file

@ -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<string | undefined>(initialJson);

View file

@ -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';
};

View file

@ -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<ObservabilityAIAssistantChatService>;
@ -18,6 +20,7 @@ export const createMockChatService = (): MockedChatService => {
chat: jest.fn(),
complete: jest.fn(),
sendAnalyticsEvent: jest.fn(),
functions$: new BehaviorSubject<FunctionDefinition[]>([]) 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;
};

View file

@ -35,5 +35,6 @@
"@kbn/code-editor",
"@kbn/ml-plugin",
"@kbn/share-plugin",
"@kbn/ai-assistant-common",
]
}

View file

@ -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;

View file

@ -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<ScreenContextActionDefinition<any>>;
starterPrompts?: StarterPrompt[];
}
export type AssistantScope = 'observability' | 'search' | 'all';

View file

@ -13,7 +13,7 @@ export function filterFunctionDefinitions({
}: {
filter?: string;
definitions: FunctionDefinition[];
}) {
}): FunctionDefinition[] {
return filter
? definitions.filter((fn) => {
const matchesFilter =

View file

@ -56,7 +56,7 @@ function ChatContent({
}) {
const service = useObservabilityAIAssistant();
const chatService = useObservabilityAIAssistantChatService();
const { scope } = service;
const scope = chatService.getScope();
const initialMessagesRef = useRef(initialMessages);

View file

@ -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<FunctionDefinition[]>([]) 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();

View file

@ -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,

View file

@ -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<ChatCompletionChunkEvent>(),
complete: (options) => new Observable<StreamingChatResponseEventWithoutError>(),
getFunctions: () => [buildFunctionElasticsearch(), buildFunctionServiceSummary()],
functions$: new BehaviorSubject<FunctionDefinition[]>([] as FunctionDefinition[]),
renderFunction: (name) => (
<div>
{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<AssistantScope>('all'),
};
function createSetupContract(): ObservabilityAIAssistantPublicSetup {

View file

@ -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<ConfigSchema>) {
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 = <P extends {}, R = {}>(

View file

@ -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<StreamingChatResponseEventWithoutError>
@ -70,7 +71,7 @@ describe('createChatService', () => {
apiClient: clientSpy,
registrations: [],
signal: new AbortController().signal,
scope: 'observability',
scope$: new BehaviorSubject<AssistantScope>('observability'),
});
});

View file

@ -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<ObservabilityAIAssistantChatService> {
const functionRegistry: FunctionRegistry = new Map();
class ChatService {
private functionRegistry: FunctionRegistry;
private renderFunctionRegistry: Map<string, RenderFunction<unknown, FunctionResponse>>;
private abortSignal: AbortSignal;
private apiClient: ObservabilityAIAssistantAPIClient;
public scope$: BehaviorSubject<AssistantScope>;
private analytics: AnalyticsServiceStart;
private registrations: ChatRegistrationRenderFunction[];
private systemMessage: string;
public functions$: BehaviorSubject<FunctionDefinition[]>;
const renderFunctionRegistry: Map<string, RenderFunction<unknown, FunctionResponse>> = new Map();
constructor({
abortSignal,
apiClient,
scope$,
analytics,
registrations,
}: {
abortSignal: AbortSignal;
apiClient: ObservabilityAIAssistantAPIClient;
scope$: BehaviorSubject<AssistantScope>;
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,
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(),
},
},
},
}),
...registrations.map((registration) => {
return registration({
registerRenderFunction: (name, renderFn) => {
renderFunctionRegistry.set(name, renderFn);
},
});
}),
]);
}),
...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;
const getFunctions = (options?: { contexts?: string[]; filter?: string }) => {
return filterFunctionDefinitions({
...options,
definitions: functionDefinitions,
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);
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,
});
};
function callStreamingApi<TEndpoint extends ObservabilityAIAssistantAPIEndpoint>(
public getFunctions = (options?: {
contexts?: string[];
filter?: string;
}): FunctionDefinition[] => {
return filterFunctionDefinitions({
...options,
definitions: Array.from(this.functionRegistry.values()),
}).filter((value) => {
return value.scopes
? value.scopes?.includes(this.getScope()) || value.scopes?.includes('all')
: true;
});
};
public callStreamingApi<TEndpoint extends ObservabilityAIAssistantAPIEndpoint>(
endpoint: TEndpoint,
options: {
signal: AbortSignal;
} & ObservabilityAIAssistantAPIClientRequestParamsOf<TEndpoint>
): Observable<StreamingChatResponseEventWithoutError> {
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<ObservabilityAIAssistantChatService, 'chat' | 'complete'> = {
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,
connectorId,
functionCall,
functions: functions ?? [],
scope: this.getScope(),
},
},
signal,
}).pipe(
filter(
(line): line is ChatCompletionChunkEvent =>
line.type === StreamingChatResponseEventType.ChatCompletionChunk
)
);
};
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,
client,
instructions,
scope,
},
({ params }) => {
return callStreamingApi('POST /internal/observability_ai_assistant/chat/complete', {
params,
signal,
});
}
);
},
};
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`);
});
}
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,
});
},
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,
);
};
public getScope() {
return this.scope$.value;
}
}
export async function createChatService({
analytics,
signal: setupAbortSignal,
registrations,
apiClient,
scope$,
}: {
analytics: AnalyticsServiceStart;
signal: AbortSignal;
registrations: ChatRegistrationRenderFunction[];
apiClient: ObservabilityAIAssistantAPIClient;
scope$: BehaviorSubject<AssistantScope>;
}): Promise<ObservabilityAIAssistantChatService> {
return new ChatService({
analytics,
apiClient,
scope$,
registrations,
abortSignal: setupAbortSignal,
});
}

View file

@ -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<ObservabilityAIAssistantChatService>;
@ -16,6 +17,7 @@ export const createMockChatService = (): MockedChatService => {
chat: jest.fn(),
complete: jest.fn(),
sendAnalyticsEvent: jest.fn(),
functions$: new BehaviorSubject<FunctionDefinition[]>([]) 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;
};

View file

@ -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<AssistantScope>(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$,
};
}

View file

@ -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'],
},
];

View file

@ -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<FunctionDefinition[]>(
[]
) 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<AssistantScope>('all') as ObservabilityAIAssistantService['scope$'],
getScope: () => 'all',
setScope: () => {},
});

View file

@ -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<FunctionDefinition[]>;
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<Observable<MessageAddEvent>>;
scope: AssistantScope;
scope$: BehaviorSubject<AssistantScope>;
setScope: (scope: AssistantScope) => void;
getScope: () => AssistantScope;
}
export type RenderFunction<TArguments, TResponse extends FunctionResponse> = (options: {
@ -120,7 +124,9 @@ export type ChatRegistrationRenderFunction = ({}: {
registerRenderFunction: RegisterRenderFunctionDefinition;
}) => Promise<void>;
export interface ConfigSchema {}
export interface ConfigSchema {
scope?: AssistantScope;
}
export interface ObservabilityAIAssistantPluginSetupDependencies {
licensing: {};

View file

@ -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<typeof config>;

View file

@ -47,7 +47,7 @@ export const config: PluginConfigDescriptor<ObservabilityAIAssistantConfig> = {
level: 'warning',
}),
],
exposeToBrowser: {},
exposeToBrowser: { scope: true },
schema: configSchema,
};

View file

@ -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';

View file

@ -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,

View file

@ -134,11 +134,14 @@ export const functionRt = t.intersection([
}),
]);
export const starterPromptRt: t.Type<StarterPrompt> = t.type({
title: t.string,
prompt: t.string,
icon: t.any,
});
export const starterPromptRt: t.Type<StarterPrompt> = t.intersection([
t.type({
title: t.string,
prompt: t.string,
icon: t.any,
}),
t.partial({ scopes: t.array(assistantScopeType) }),
]);
export const screenContextRt: t.Type<ObservabilityAIAssistantScreenContextRequest> = t.partial({
description: t.string,

View file

@ -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);

View file

@ -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';

View file

@ -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(

View file

@ -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';

View file

@ -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';

View file

@ -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/**/*"]
}

View file

@ -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 }) => {

View file

@ -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) {

View file

@ -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<string, AssistantScope> = {
[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]);
}

View file

@ -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,

View file

@ -68,6 +68,7 @@
"@kbn/task-manager-plugin",
"@kbn/cloud-plugin",
"@kbn/logs-data-access-plugin",
"@kbn/ai-assistant-common",
],
"exclude": [
"target/**/*"

View file

@ -21,6 +21,7 @@
],
"optionalPlugins": [
"cloud",
"serverless",
"usageCollection",
],
"requiredBundles": [

View file

@ -30,6 +30,7 @@ export function ConversationViewWithProps() {
getConversationHref={(id: string) =>
http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || ''
}
scope="search"
/>
);
}

View file

@ -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'],
});
};

View file

@ -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';

View file

@ -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<SearchAssistantPluginSetup, SearchAssistantPluginStart>
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 {};
}

View file

@ -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;
}

View file

@ -22,7 +22,8 @@
"@kbn/config-schema",
"@kbn/ai-assistant",
"@kbn/i18n",
"@kbn/shared-ux-router"
"@kbn/shared-ux-router",
"@kbn/serverless"
],
"exclude": [
"target/**/*",

View file

@ -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) {

View file

@ -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"
]
}

View file

@ -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';

View file

@ -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",
]
}

View file

@ -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 ""