mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[AI Assistant] Add assistant to Serverless Search (#196832)
## Summary This adds the AI assistant to Serverless Elasticsearch. It also disables the knowledge base, and disables a few config values we don't want users to be able to set in that context. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elena Shostak <165678770+elena-shostak@users.noreply.github.com>
This commit is contained in:
parent
27a98aa9c3
commit
3bc5e2db73
50 changed files with 642 additions and 352 deletions
|
@ -10,7 +10,6 @@ xpack.observability.enabled: false
|
||||||
xpack.securitySolution.enabled: false
|
xpack.securitySolution.enabled: false
|
||||||
xpack.serverless.observability.enabled: false
|
xpack.serverless.observability.enabled: false
|
||||||
enterpriseSearch.enabled: false
|
enterpriseSearch.enabled: false
|
||||||
xpack.observabilityAIAssistant.enabled: false
|
|
||||||
xpack.osquery.enabled: false
|
xpack.osquery.enabled: false
|
||||||
|
|
||||||
# Enable fleet on search projects for agentless features
|
# Enable fleet on search projects for agentless features
|
||||||
|
@ -120,4 +119,16 @@ xpack.searchInferenceEndpoints.ui.enabled: false
|
||||||
xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json
|
xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json
|
||||||
|
|
||||||
# Semantic text UI
|
# Semantic text UI
|
||||||
|
|
||||||
xpack.index_management.dev.enableSemanticText: true
|
xpack.index_management.dev.enableSemanticText: true
|
||||||
|
|
||||||
|
# AI Assistant config
|
||||||
|
xpack.observabilityAIAssistant.enabled: true
|
||||||
|
xpack.searchAssistant.enabled: true
|
||||||
|
xpack.searchAssistant.ui.enabled: true
|
||||||
|
xpack.observabilityAIAssistant.scope: "search"
|
||||||
|
xpack.observabilityAIAssistant.enableKnowledgeBase: false
|
||||||
|
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
|
||||||
|
xpack.observabilityAiAssistantManagement.logSourcesEnabled: false
|
||||||
|
xpack.observabilityAiAssistantManagement.spacesEnabled: false
|
||||||
|
xpack.observabilityAiAssistantManagement.visibilityEnabled: false
|
||||||
|
|
|
@ -183,6 +183,7 @@ xpack.apm.featureFlags.storageExplorerAvailable: false
|
||||||
|
|
||||||
## Set the AI Assistant type
|
## Set the AI Assistant type
|
||||||
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
|
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
|
||||||
|
xpack.observabilityAIAssistant.scope: "observability"
|
||||||
|
|
||||||
# Specify in telemetry the project type
|
# Specify in telemetry the project type
|
||||||
telemetry.labels.serverless: observability
|
telemetry.labels.serverless: observability
|
||||||
|
|
|
@ -362,6 +362,9 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
||||||
'xpack.observability_onboarding.ui.enabled (boolean?)',
|
'xpack.observability_onboarding.ui.enabled (boolean?)',
|
||||||
'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)',
|
'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)',
|
||||||
'xpack.observabilityAIAssistant.scope (observability?|search?)',
|
'xpack.observabilityAIAssistant.scope (observability?|search?)',
|
||||||
|
'xpack.observabilityAiAssistantManagement.logSourcesEnabled (boolean?)',
|
||||||
|
'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)',
|
||||||
|
'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)',
|
||||||
'share.new_version.enabled (boolean?)',
|
'share.new_version.enabled (boolean?)',
|
||||||
'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)',
|
'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)',
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public';
|
import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public';
|
||||||
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
|
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
|
||||||
import { useKibana } from '../hooks/use_kibana';
|
import { useKibana } from '../hooks/use_kibana';
|
||||||
|
import { useKnowledgeBase } from '../hooks';
|
||||||
|
|
||||||
export function ChatActionsMenu({
|
export function ChatActionsMenu({
|
||||||
connectors,
|
connectors,
|
||||||
|
@ -31,6 +32,7 @@ export function ChatActionsMenu({
|
||||||
onCopyConversationClick: () => void;
|
onCopyConversationClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { application, http } = useKibana().services;
|
const { application, http } = useKibana().services;
|
||||||
|
const knowledgeBase = useKnowledgeBase();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const handleNavigateToConnectors = () => {
|
const handleNavigateToConnectors = () => {
|
||||||
|
@ -91,15 +93,19 @@ export function ChatActionsMenu({
|
||||||
defaultMessage: 'Actions',
|
defaultMessage: 'Actions',
|
||||||
}),
|
}),
|
||||||
items: [
|
items: [
|
||||||
{
|
...(knowledgeBase?.status.value?.enabled
|
||||||
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', {
|
? [
|
||||||
defaultMessage: 'Manage knowledge base',
|
{
|
||||||
}),
|
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', {
|
||||||
onClick: () => {
|
defaultMessage: 'Manage knowledge base',
|
||||||
toggleActionsMenu();
|
}),
|
||||||
handleNavigateToSettingsKnowledgeBase();
|
onClick: () => {
|
||||||
},
|
toggleActionsMenu();
|
||||||
},
|
handleNavigateToSettingsKnowledgeBase();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', {
|
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', {
|
||||||
defaultMessage: 'AI Assistant Settings',
|
defaultMessage: 'AI Assistant Settings',
|
||||||
|
|
|
@ -37,6 +37,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
|
||||||
loading: false,
|
loading: false,
|
||||||
value: {
|
value: {
|
||||||
ready: true,
|
ready: true,
|
||||||
|
enabled: true,
|
||||||
},
|
},
|
||||||
refresh: () => {},
|
refresh: () => {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -123,7 +123,7 @@ export function ChatBody({
|
||||||
showLinkToConversationsApp: boolean;
|
showLinkToConversationsApp: boolean;
|
||||||
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
|
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
|
||||||
onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void;
|
onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void;
|
||||||
navigateToConversation: (conversationId?: string) => void;
|
navigateToConversation?: (conversationId?: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const license = useLicense();
|
const license = useLicense();
|
||||||
const hasCorrectLicense = license?.hasAtLeast('enterprise');
|
const hasCorrectLicense = license?.hasAtLeast('enterprise');
|
||||||
|
|
|
@ -53,7 +53,7 @@ export function ChatFlyout({
|
||||||
initialFlyoutPositionMode?: FlyoutPositionMode;
|
initialFlyoutPositionMode?: FlyoutPositionMode;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
navigateToConversation(conversationId?: string): void;
|
navigateToConversation?: (conversationId?: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { euiTheme } = useEuiTheme();
|
const { euiTheme } = useEuiTheme();
|
||||||
const breakpoint = useCurrentEuiBreakpoint();
|
const breakpoint = useCurrentEuiBreakpoint();
|
||||||
|
@ -272,10 +272,14 @@ export function ChatFlyout({
|
||||||
conversationList.conversations.refresh();
|
conversationList.conversations.refresh();
|
||||||
}}
|
}}
|
||||||
onToggleFlyoutPositionMode={handleToggleFlyoutPositionMode}
|
onToggleFlyoutPositionMode={handleToggleFlyoutPositionMode}
|
||||||
navigateToConversation={(newConversationId?: string) => {
|
navigateToConversation={
|
||||||
if (onClose) onClose();
|
navigateToConversation
|
||||||
navigateToConversation(newConversationId);
|
? (newConversationId?: string) => {
|
||||||
}}
|
if (onClose) onClose();
|
||||||
|
navigateToConversation(newConversationId);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ export function ChatHeader({
|
||||||
onCopyConversation: () => void;
|
onCopyConversation: () => void;
|
||||||
onSaveTitle: (title: string) => void;
|
onSaveTitle: (title: string) => void;
|
||||||
onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void;
|
onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void;
|
||||||
navigateToConversation: (nextConversationId?: string) => void;
|
navigateToConversation?: (nextConversationId?: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const theme = useEuiTheme();
|
const theme = useEuiTheme();
|
||||||
const breakpoint = useCurrentEuiBreakpoint();
|
const breakpoint = useCurrentEuiBreakpoint();
|
||||||
|
@ -164,31 +164,32 @@ export function ChatHeader({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
{navigateToConversation ? (
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiPopover
|
<EuiPopover
|
||||||
anchorPosition="downLeft"
|
anchorPosition="downLeft"
|
||||||
button={
|
button={
|
||||||
<EuiToolTip
|
<EuiToolTip
|
||||||
content={i18n.translate(
|
content={i18n.translate(
|
||||||
'xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel',
|
'xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel',
|
||||||
{ defaultMessage: 'Navigate to conversations' }
|
|
||||||
)}
|
|
||||||
display="block"
|
|
||||||
>
|
|
||||||
<EuiButtonIcon
|
|
||||||
aria-label={i18n.translate(
|
|
||||||
'xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel',
|
|
||||||
{ defaultMessage: 'Navigate to conversations' }
|
{ defaultMessage: 'Navigate to conversations' }
|
||||||
)}
|
)}
|
||||||
data-test-subj="observabilityAiAssistantChatHeaderButton"
|
display="block"
|
||||||
iconType="discuss"
|
>
|
||||||
onClick={() => navigateToConversation(conversationId)}
|
<EuiButtonIcon
|
||||||
/>
|
aria-label={i18n.translate(
|
||||||
</EuiToolTip>
|
'xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel',
|
||||||
}
|
{ defaultMessage: 'Navigate to conversations' }
|
||||||
/>
|
)}
|
||||||
</EuiFlexItem>
|
data-test-subj="observabilityAiAssistantChatHeaderButton"
|
||||||
|
iconType="discuss"
|
||||||
|
onClick={() => navigateToConversation(conversationId)}
|
||||||
|
/>
|
||||||
|
</EuiToolTip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ const defaultProps: ComponentProps<typeof Component> = {
|
||||||
loading: false,
|
loading: false,
|
||||||
value: {
|
value: {
|
||||||
ready: true,
|
ready: true,
|
||||||
|
enabled: true,
|
||||||
},
|
},
|
||||||
refresh: () => {},
|
refresh: () => {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
|
||||||
loading: false,
|
loading: false,
|
||||||
value: {
|
value: {
|
||||||
ready: false,
|
ready: false,
|
||||||
|
enabled: true,
|
||||||
},
|
},
|
||||||
refresh: () => {},
|
refresh: () => {},
|
||||||
},
|
},
|
||||||
|
@ -43,12 +44,15 @@ export const Loading: ComponentStoryObj<typeof Component> = merge({}, defaultPro
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NotInstalled: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
|
export const NotInstalled: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
|
||||||
args: { knowledgeBase: { status: { loading: false, value: { ready: false } } } },
|
args: { knowledgeBase: { status: { loading: false, value: { ready: false, enabled: true } } } },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Installing: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
|
export const Installing: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
|
||||||
args: {
|
args: {
|
||||||
knowledgeBase: { status: { loading: false, value: { ready: false } }, isInstalling: true },
|
knowledgeBase: {
|
||||||
|
status: { loading: false, value: { ready: false, enabled: true } },
|
||||||
|
isInstalling: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -57,7 +61,7 @@ export const InstallError: ComponentStoryObj<typeof Component> = merge({}, defau
|
||||||
knowledgeBase: {
|
knowledgeBase: {
|
||||||
status: {
|
status: {
|
||||||
loading: false,
|
loading: false,
|
||||||
value: { ready: false },
|
value: { ready: false, enabled: true },
|
||||||
},
|
},
|
||||||
isInstalling: false,
|
isInstalling: false,
|
||||||
installError: new Error(),
|
installError: new Error(),
|
||||||
|
@ -66,5 +70,5 @@ export const InstallError: ComponentStoryObj<typeof Component> = merge({}, defau
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Installed: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
|
export const Installed: ComponentStoryObj<typeof Component> = merge({}, defaultProps, {
|
||||||
args: { knowledgeBase: { status: { loading: false, value: { ready: true } } } },
|
args: { knowledgeBase: { status: { loading: false, value: { ready: true, enabled: true } } } },
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,8 +85,9 @@ export function WelcomeMessage({
|
||||||
connectors={connectors}
|
connectors={connectors}
|
||||||
onSetupConnectorClick={handleConnectorClick}
|
onSetupConnectorClick={handleConnectorClick}
|
||||||
/>
|
/>
|
||||||
|
{knowledgeBase.status.value?.enabled ? (
|
||||||
<WelcomeMessageKnowledgeBase connectors={connectors} knowledgeBase={knowledgeBase} />
|
<WelcomeMessageKnowledgeBase connectors={connectors} knowledgeBase={knowledgeBase} />
|
||||||
|
) : null}
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
|
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
|
|
|
@ -25,7 +25,7 @@ const SECOND_SLOT_CONTAINER_WIDTH = 400;
|
||||||
|
|
||||||
interface ConversationViewProps {
|
interface ConversationViewProps {
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
navigateToConversation: (nextConversationId?: string) => void;
|
navigateToConversation?: (nextConversationId?: string) => void;
|
||||||
getConversationHref?: (conversationId: string) => string;
|
getConversationHref?: (conversationId: string) => string;
|
||||||
newConversationHref?: string;
|
newConversationHref?: string;
|
||||||
scopes?: AssistantScope[];
|
scopes?: AssistantScope[];
|
||||||
|
@ -81,7 +81,9 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
|
||||||
const handleConversationUpdate = (conversation: { conversation: { id: string } }) => {
|
const handleConversationUpdate = (conversation: { conversation: { id: string } }) => {
|
||||||
if (!conversationId) {
|
if (!conversationId) {
|
||||||
updateConversationIdInPlace(conversation.conversation.id);
|
updateConversationIdInPlace(conversation.conversation.id);
|
||||||
navigateToConversation(conversation.conversation.id);
|
if (navigateToConversation) {
|
||||||
|
navigateToConversation(conversation.conversation.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handleRefreshConversations();
|
handleRefreshConversations();
|
||||||
};
|
};
|
||||||
|
@ -143,7 +145,7 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
|
||||||
isLoading={conversationList.isLoading}
|
isLoading={conversationList.isLoading}
|
||||||
onConversationDeleteClick={(deletedConversationId) => {
|
onConversationDeleteClick={(deletedConversationId) => {
|
||||||
conversationList.deleteConversation(deletedConversationId).then(() => {
|
conversationList.deleteConversation(deletedConversationId).then(() => {
|
||||||
if (deletedConversationId === conversationId) {
|
if (deletedConversationId === conversationId && navigateToConversation) {
|
||||||
navigateToConversation(undefined);
|
navigateToConversation(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult {
|
||||||
error: undefined,
|
error: undefined,
|
||||||
value: {
|
value: {
|
||||||
ready: true,
|
ready: true,
|
||||||
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { useAIAssistantAppService } from './use_ai_assistant_app_service';
|
||||||
export interface UseKnowledgeBaseResult {
|
export interface UseKnowledgeBaseResult {
|
||||||
status: AbortableAsyncState<{
|
status: AbortableAsyncState<{
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
enabled: boolean;
|
||||||
error?: any;
|
error?: any;
|
||||||
deployment_state?: MlDeploymentState;
|
deployment_state?: MlDeploymentState;
|
||||||
allocation_state?: MlDeploymentAllocationState;
|
allocation_state?: MlDeploymentAllocationState;
|
||||||
|
|
|
@ -11,6 +11,7 @@ export const config = schema.object({
|
||||||
enabled: schema.boolean({ defaultValue: true }),
|
enabled: schema.boolean({ defaultValue: true }),
|
||||||
modelId: schema.maybe(schema.string()),
|
modelId: schema.maybe(schema.string()),
|
||||||
scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])),
|
scope: schema.maybe(schema.oneOf([schema.literal('observability'), schema.literal('search')])),
|
||||||
|
enableKnowledgeBase: schema.boolean({ defaultValue: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ObservabilityAIAssistantConfig = TypeOf<typeof config>;
|
export type ObservabilityAIAssistantConfig = TypeOf<typeof config>;
|
||||||
|
|
|
@ -159,11 +159,14 @@ export class ObservabilityAIAssistantPlugin
|
||||||
core,
|
core,
|
||||||
taskManager: plugins.taskManager,
|
taskManager: plugins.taskManager,
|
||||||
getModelId,
|
getModelId,
|
||||||
|
enableKnowledgeBase: this.config.enableKnowledgeBase,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
service.register(registerFunctions);
|
service.register(registerFunctions);
|
||||||
|
|
||||||
addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') });
|
if (this.config.enableKnowledgeBase) {
|
||||||
|
addLensDocsToKb({ service, logger: this.logger.get('kb').get('lens') });
|
||||||
|
}
|
||||||
|
|
||||||
registerServerRoutes({
|
registerServerRoutes({
|
||||||
core,
|
core,
|
||||||
|
|
|
@ -28,6 +28,7 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({
|
||||||
handler: async (
|
handler: async (
|
||||||
resources
|
resources
|
||||||
): Promise<{
|
): Promise<{
|
||||||
|
enabled: boolean;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
error?: any;
|
error?: any;
|
||||||
deployment_state?: MlDeploymentState;
|
deployment_state?: MlDeploymentState;
|
||||||
|
|
|
@ -707,14 +707,16 @@ export class ObservabilityAIAssistantClient {
|
||||||
queries: Array<{ text: string; boost?: number }>;
|
queries: Array<{ text: string; boost?: number }>;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
}): Promise<{ entries: RecalledEntry[] }> => {
|
}): Promise<{ entries: RecalledEntry[] }> => {
|
||||||
return this.dependencies.knowledgeBaseService.recall({
|
return (
|
||||||
namespace: this.dependencies.namespace,
|
this.dependencies.knowledgeBaseService?.recall({
|
||||||
user: this.dependencies.user,
|
namespace: this.dependencies.namespace,
|
||||||
queries,
|
user: this.dependencies.user,
|
||||||
categories,
|
queries,
|
||||||
esClient: this.dependencies.esClient,
|
categories,
|
||||||
uiSettingsClient: this.dependencies.uiSettingsClient,
|
esClient: this.dependencies.esClient,
|
||||||
});
|
uiSettingsClient: this.dependencies.uiSettingsClient,
|
||||||
|
}) || { entries: [] }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getKnowledgeBaseStatus = () => {
|
getKnowledgeBaseStatus = () => {
|
||||||
|
|
|
@ -70,6 +70,7 @@ export class ObservabilityAIAssistantService {
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
private readonly getModelId: () => Promise<string>;
|
private readonly getModelId: () => Promise<string>;
|
||||||
private kbService?: KnowledgeBaseService;
|
private kbService?: KnowledgeBaseService;
|
||||||
|
private enableKnowledgeBase: boolean;
|
||||||
|
|
||||||
private readonly registrations: RegistrationCallback[] = [];
|
private readonly registrations: RegistrationCallback[] = [];
|
||||||
|
|
||||||
|
@ -78,36 +79,40 @@ export class ObservabilityAIAssistantService {
|
||||||
core,
|
core,
|
||||||
taskManager,
|
taskManager,
|
||||||
getModelId,
|
getModelId,
|
||||||
|
enableKnowledgeBase,
|
||||||
}: {
|
}: {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
core: CoreSetup<ObservabilityAIAssistantPluginStartDependencies>;
|
core: CoreSetup<ObservabilityAIAssistantPluginStartDependencies>;
|
||||||
taskManager: TaskManagerSetupContract;
|
taskManager: TaskManagerSetupContract;
|
||||||
getModelId: () => Promise<string>;
|
getModelId: () => Promise<string>;
|
||||||
|
enableKnowledgeBase: boolean;
|
||||||
}) {
|
}) {
|
||||||
this.core = core;
|
this.core = core;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.getModelId = getModelId;
|
this.getModelId = getModelId;
|
||||||
|
this.enableKnowledgeBase = enableKnowledgeBase;
|
||||||
|
|
||||||
this.allowInit();
|
this.allowInit();
|
||||||
|
if (enableKnowledgeBase) {
|
||||||
taskManager.registerTaskDefinitions({
|
taskManager.registerTaskDefinitions({
|
||||||
[INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: {
|
[INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: {
|
||||||
title: 'Index queued KB articles',
|
title: 'Index queued KB articles',
|
||||||
description:
|
description:
|
||||||
'Indexes previously registered entries into the knowledge base when it is ready',
|
'Indexes previously registered entries into the knowledge base when it is ready',
|
||||||
timeout: '30m',
|
timeout: '30m',
|
||||||
maxAttempts: 2,
|
maxAttempts: 2,
|
||||||
createTaskRunner: (context) => {
|
createTaskRunner: (context) => {
|
||||||
return {
|
return {
|
||||||
run: async () => {
|
run: async () => {
|
||||||
if (this.kbService) {
|
if (this.kbService) {
|
||||||
await this.kbService.processQueue();
|
await this.kbService.processQueue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getKnowledgeBaseStatus() {
|
getKnowledgeBaseStatus() {
|
||||||
|
@ -237,6 +242,7 @@ export class ObservabilityAIAssistantService {
|
||||||
esClient,
|
esClient,
|
||||||
taskManagerStart: pluginsStart.taskManager,
|
taskManagerStart: pluginsStart.taskManager,
|
||||||
getModelId: this.getModelId,
|
getModelId: this.getModelId,
|
||||||
|
enabled: this.enableKnowledgeBase,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.info('Successfully set up index assets');
|
this.logger.info('Successfully set up index assets');
|
||||||
|
@ -331,58 +337,62 @@ export class ObservabilityAIAssistantService {
|
||||||
}
|
}
|
||||||
|
|
||||||
addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void {
|
addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void {
|
||||||
this.init()
|
if (this.enableKnowledgeBase) {
|
||||||
.then(() => {
|
this.init()
|
||||||
this.kbService!.queue(
|
.then(() => {
|
||||||
entries.flatMap((entry) => {
|
this.kbService!.queue(
|
||||||
const entryWithSystemProperties = {
|
entries.flatMap((entry) => {
|
||||||
...entry,
|
const entryWithSystemProperties = {
|
||||||
'@timestamp': new Date().toISOString(),
|
...entry,
|
||||||
doc_id: entry.id,
|
'@timestamp': new Date().toISOString(),
|
||||||
public: true,
|
doc_id: entry.id,
|
||||||
confidence: 'high' as const,
|
public: true,
|
||||||
type: 'contextual' as const,
|
confidence: 'high' as const,
|
||||||
is_correction: false,
|
type: 'contextual' as const,
|
||||||
labels: {
|
is_correction: false,
|
||||||
...entry.labels,
|
labels: {
|
||||||
},
|
...entry.labels,
|
||||||
role: KnowledgeBaseEntryRole.Elastic,
|
},
|
||||||
};
|
role: KnowledgeBaseEntryRole.Elastic,
|
||||||
|
};
|
||||||
|
|
||||||
const operations =
|
const operations =
|
||||||
'texts' in entryWithSystemProperties
|
'texts' in entryWithSystemProperties
|
||||||
? splitKbText(entryWithSystemProperties)
|
? splitKbText(entryWithSystemProperties)
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
type: KnowledgeBaseEntryOperationType.Index,
|
type: KnowledgeBaseEntryOperationType.Index,
|
||||||
document: entryWithSystemProperties,
|
document: entryWithSystemProperties,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return operations;
|
return operations;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Could not index ${entries.length} entries because of an initialisation error`
|
`Could not index ${entries.length} entries because of an initialisation error`
|
||||||
);
|
);
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) {
|
addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) {
|
||||||
this.addToKnowledgeBaseQueue(
|
if (this.enableKnowledgeBase) {
|
||||||
entries.map((entry) => {
|
this.addToKnowledgeBaseQueue(
|
||||||
return {
|
entries.map((entry) => {
|
||||||
...entry,
|
return {
|
||||||
labels: {
|
...entry,
|
||||||
...entry.labels,
|
labels: {
|
||||||
category: categoryId,
|
...entry.labels,
|
||||||
},
|
category: categoryId,
|
||||||
};
|
},
|
||||||
})
|
};
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
register(cb: RegistrationCallback) {
|
register(cb: RegistrationCallback) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ interface Dependencies {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
taskManagerStart: TaskManagerStartContract;
|
taskManagerStart: TaskManagerStartContract;
|
||||||
getModelId: () => Promise<string>;
|
getModelId: () => Promise<string>;
|
||||||
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecalledEntry {
|
export interface RecalledEntry {
|
||||||
|
@ -92,6 +93,9 @@ export class KnowledgeBaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
setup = async () => {
|
setup = async () => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const elserModelId = await this.dependencies.getModelId();
|
const elserModelId = await this.dependencies.getModelId();
|
||||||
|
|
||||||
const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 };
|
const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 };
|
||||||
|
@ -113,9 +117,9 @@ export class KnowledgeBaseService {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isModelMissingOrUnavailableError(error)) {
|
if (isModelMissingOrUnavailableError(error)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -202,6 +206,9 @@ export class KnowledgeBaseService {
|
||||||
};
|
};
|
||||||
|
|
||||||
private ensureTaskScheduled() {
|
private ensureTaskScheduled() {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.dependencies.taskManagerStart
|
this.dependencies.taskManagerStart
|
||||||
.ensureScheduled({
|
.ensureScheduled({
|
||||||
taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE,
|
taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE,
|
||||||
|
@ -251,7 +258,7 @@ export class KnowledgeBaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async processQueue() {
|
async processQueue() {
|
||||||
if (!this._queue.length) {
|
if (!this._queue.length || !this.dependencies.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,6 +312,9 @@ export class KnowledgeBaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
status = async () => {
|
status = async () => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return { ready: false, enabled: false };
|
||||||
|
}
|
||||||
const elserModelId = await this.dependencies.getModelId();
|
const elserModelId = await this.dependencies.getModelId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -320,11 +330,13 @@ export class KnowledgeBaseService {
|
||||||
deployment_state: deploymentState,
|
deployment_state: deploymentState,
|
||||||
allocation_state: allocationState,
|
allocation_state: allocationState,
|
||||||
model_name: elserModelId,
|
model_name: elserModelId,
|
||||||
|
enabled: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
error: error instanceof errors.ResponseError ? error.body.error : String(error),
|
error: error instanceof errors.ResponseError ? error.body.error : String(error),
|
||||||
ready: false,
|
ready: false,
|
||||||
|
enabled: true,
|
||||||
model_name: elserModelId,
|
model_name: elserModelId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -402,6 +414,9 @@ export class KnowledgeBaseService {
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
entries: RecalledEntry[];
|
entries: RecalledEntry[];
|
||||||
}> => {
|
}> => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return { entries: [] };
|
||||||
|
}
|
||||||
this.dependencies.logger.debug(
|
this.dependencies.logger.debug(
|
||||||
() => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"`
|
() => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"`
|
||||||
);
|
);
|
||||||
|
@ -474,6 +489,9 @@ export class KnowledgeBaseService {
|
||||||
namespace: string,
|
namespace: string,
|
||||||
user?: { name: string }
|
user?: { name: string }
|
||||||
): Promise<Array<Instruction & { public?: boolean }>> => {
|
): Promise<Array<Instruction & { public?: boolean }>> => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
||||||
index: resourceNames.aliases.kb,
|
index: resourceNames.aliases.kb,
|
||||||
|
@ -514,6 +532,9 @@ export class KnowledgeBaseService {
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortDirection?: 'asc' | 'desc';
|
sortDirection?: 'asc' | 'desc';
|
||||||
}): Promise<{ entries: KnowledgeBaseEntry[] }> => {
|
}): Promise<{ entries: KnowledgeBaseEntry[] }> => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return { entries: [] };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
||||||
index: resourceNames.aliases.kb,
|
index: resourceNames.aliases.kb,
|
||||||
|
@ -578,6 +599,9 @@ export class KnowledgeBaseService {
|
||||||
user?: { name: string; id?: string };
|
user?: { name: string; id?: string };
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const res = await this.dependencies.esClient.asInternalUser.search<
|
const res = await this.dependencies.esClient.asInternalUser.search<
|
||||||
Pick<KnowledgeBaseEntry, 'doc_id'>
|
Pick<KnowledgeBaseEntry, 'doc_id'>
|
||||||
>({
|
>({
|
||||||
|
@ -607,6 +631,9 @@ export class KnowledgeBaseService {
|
||||||
user?: { name: string; id?: string };
|
user?: { name: string; id?: string };
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// for now we want to limit the number of user instructions to 1 per user
|
// for now we want to limit the number of user instructions to 1 per user
|
||||||
if (document.type === KnowledgeBaseType.UserInstruction) {
|
if (document.type === KnowledgeBaseType.UserInstruction) {
|
||||||
const existingId = await this.getExistingUserInstructionId({
|
const existingId = await this.getExistingUserInstructionId({
|
||||||
|
@ -647,6 +674,9 @@ export class KnowledgeBaseService {
|
||||||
}: {
|
}: {
|
||||||
operations: KnowledgeBaseEntryOperation[];
|
operations: KnowledgeBaseEntryOperation[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
|
if (!this.dependencies.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.dependencies.logger.info(`Starting import of ${operations.length} entries`);
|
this.dependencies.logger.info(`Starting import of ${operations.length} entries`);
|
||||||
|
|
||||||
const limiter = pLimit(5);
|
const limiter = pLimit(5);
|
||||||
|
|
|
@ -164,7 +164,7 @@ export function NavControl() {
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
navigateToConversation={(conversationId: string) => {
|
navigateToConversation={(conversationId?: string) => {
|
||||||
application.navigateToUrl(
|
application.navigateToUrl(
|
||||||
http.basePath.prepend(
|
http.basePath.prepend(
|
||||||
`/app/observabilityAIAssistant/conversations/${conversationId || ''}`
|
`/app/observabilityAIAssistant/conversations/${conversationId || ''}`
|
||||||
|
|
|
@ -6,9 +6,24 @@
|
||||||
"id": "observabilityAiAssistantManagement",
|
"id": "observabilityAiAssistantManagement",
|
||||||
"server": true,
|
"server": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"configPath": ["xpack", "observabilityAiAssistantManagement"],
|
"configPath": [
|
||||||
"requiredPlugins": ["management", "observabilityAIAssistant", "observabilityShared"],
|
"xpack",
|
||||||
"optionalPlugins": ["actions", "home", "serverless", "enterpriseSearch"],
|
"observabilityAiAssistantManagement"
|
||||||
"requiredBundles": ["kibanaReact", "logsDataAccess"]
|
],
|
||||||
|
"requiredPlugins": [
|
||||||
|
"actions",
|
||||||
|
"management",
|
||||||
|
"observabilityAIAssistant",
|
||||||
|
"observabilityShared"
|
||||||
|
],
|
||||||
|
"optionalPlugins": [
|
||||||
|
"home",
|
||||||
|
"serverless",
|
||||||
|
"enterpriseSearch"
|
||||||
|
],
|
||||||
|
"requiredBundles": [
|
||||||
|
"kibanaReact",
|
||||||
|
"logsDataAccess",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,11 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { CoreSetup } from '@kbn/core/public';
|
import { CoreSetup } from '@kbn/core/public';
|
||||||
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
|
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
|
||||||
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
|
||||||
import { StartDependencies, AiAssistantManagementObservabilityPluginStart } from './plugin';
|
import {
|
||||||
|
StartDependencies,
|
||||||
|
AiAssistantManagementObservabilityPluginStart,
|
||||||
|
ConfigSchema,
|
||||||
|
} from './plugin';
|
||||||
import { aIAssistantManagementObservabilityRouter } from './routes/config';
|
import { aIAssistantManagementObservabilityRouter } from './routes/config';
|
||||||
import { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized';
|
import { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized';
|
||||||
import { AppContextProvider } from './context/app_context';
|
import { AppContextProvider } from './context/app_context';
|
||||||
|
@ -23,9 +27,10 @@ import { AppContextProvider } from './context/app_context';
|
||||||
interface MountParams {
|
interface MountParams {
|
||||||
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>;
|
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>;
|
||||||
mountParams: ManagementAppMountParams;
|
mountParams: ManagementAppMountParams;
|
||||||
|
config: ConfigSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mountManagementSection = async ({ core, mountParams }: MountParams) => {
|
export const mountManagementSection = async ({ core, mountParams, config }: MountParams) => {
|
||||||
const [coreStart, startDeps] = await core.getStartServices();
|
const [coreStart, startDeps] = await core.getStartServices();
|
||||||
|
|
||||||
if (!startDeps.observabilityAIAssistant) return () => {};
|
if (!startDeps.observabilityAIAssistant) return () => {};
|
||||||
|
@ -46,7 +51,7 @@ export const mountManagementSection = async ({ core, mountParams }: MountParams)
|
||||||
<RedirectToHomeIfUnauthorized coreStart={coreStart}>
|
<RedirectToHomeIfUnauthorized coreStart={coreStart}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<KibanaContextProvider services={{ ...coreStart, ...startDeps }}>
|
<KibanaContextProvider services={{ ...coreStart, ...startDeps }}>
|
||||||
<AppContextProvider value={{ setBreadcrumbs }}>
|
<AppContextProvider value={{ setBreadcrumbs, config }}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
history={history}
|
history={history}
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
|
|
||||||
import React, { createContext } from 'react';
|
import React, { createContext } from 'react';
|
||||||
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
|
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
|
||||||
|
import { ConfigSchema } from '../plugin';
|
||||||
|
|
||||||
export interface AppContextValue {
|
export interface AppContextValue {
|
||||||
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
|
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
|
||||||
|
config: ConfigSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContext = createContext<AppContextValue>(null as any);
|
export const AppContext = createContext<AppContextValue>(null as any);
|
||||||
|
|
|
@ -70,6 +70,11 @@ export const render = (
|
||||||
|
|
||||||
const appContextValue = mocks?.appContextValue ?? {
|
const appContextValue = mocks?.appContextValue ?? {
|
||||||
setBreadcrumbs: () => {},
|
setBreadcrumbs: () => {},
|
||||||
|
config: {
|
||||||
|
logSourcesEnabled: true,
|
||||||
|
spacesEnabled: true,
|
||||||
|
visibilityEnabled: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return testLibRender(
|
return testLibRender(
|
||||||
|
|
|
@ -5,13 +5,26 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AiAssistantManagementObservabilityPlugin as AiAssistantManagementObservabilityPlugin } from './plugin';
|
import { PluginInitializer, PluginInitializerContext } from '@kbn/core-plugins-browser';
|
||||||
|
import {
|
||||||
|
AiAssistantManagementObservabilityPlugin,
|
||||||
|
AiAssistantManagementObservabilityPluginSetup,
|
||||||
|
AiAssistantManagementObservabilityPluginStart,
|
||||||
|
ConfigSchema,
|
||||||
|
SetupDependencies,
|
||||||
|
StartDependencies,
|
||||||
|
} from './plugin';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AiAssistantManagementObservabilityPluginSetup,
|
AiAssistantManagementObservabilityPluginSetup,
|
||||||
AiAssistantManagementObservabilityPluginStart,
|
AiAssistantManagementObservabilityPluginStart,
|
||||||
} from './plugin';
|
} from './plugin';
|
||||||
|
|
||||||
export function plugin() {
|
export const plugin: PluginInitializer<
|
||||||
return new AiAssistantManagementObservabilityPlugin();
|
AiAssistantManagementObservabilityPluginSetup,
|
||||||
}
|
AiAssistantManagementObservabilityPluginStart,
|
||||||
|
SetupDependencies,
|
||||||
|
StartDependencies
|
||||||
|
> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) => {
|
||||||
|
return new AiAssistantManagementObservabilityPlugin(pluginInitializerContext);
|
||||||
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { CoreSetup, Plugin } from '@kbn/core/public';
|
import { CoreSetup, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||||
import { ManagementSetup } from '@kbn/management-plugin/public';
|
import { ManagementSetup } from '@kbn/management-plugin/public';
|
||||||
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
import { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
||||||
import { ServerlessPluginStart } from '@kbn/serverless/public';
|
import { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||||
|
@ -35,6 +35,12 @@ export interface StartDependencies {
|
||||||
enterpriseSearch?: EnterpriseSearchPublicStart;
|
enterpriseSearch?: EnterpriseSearchPublicStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigSchema {
|
||||||
|
logSourcesEnabled: boolean;
|
||||||
|
spacesEnabled: boolean;
|
||||||
|
visibilityEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class AiAssistantManagementObservabilityPlugin
|
export class AiAssistantManagementObservabilityPlugin
|
||||||
implements
|
implements
|
||||||
Plugin<
|
Plugin<
|
||||||
|
@ -44,6 +50,12 @@ export class AiAssistantManagementObservabilityPlugin
|
||||||
StartDependencies
|
StartDependencies
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
private readonly config: ConfigSchema;
|
||||||
|
|
||||||
|
constructor(context: PluginInitializerContext<ConfigSchema>) {
|
||||||
|
this.config = context.config.get();
|
||||||
|
}
|
||||||
|
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>,
|
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>,
|
||||||
{ home, management, observabilityAIAssistant }: SetupDependencies
|
{ home, management, observabilityAIAssistant }: SetupDependencies
|
||||||
|
@ -78,6 +90,7 @@ export class AiAssistantManagementObservabilityPlugin
|
||||||
return mountManagementSection({
|
return mountManagementSection({
|
||||||
core,
|
core,
|
||||||
mountParams,
|
mountParams,
|
||||||
|
config: this.config,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,8 +8,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { coreStartMock, render } from '../../helpers/test_helper';
|
import { coreStartMock, render } from '../../helpers/test_helper';
|
||||||
import { SettingsPage } from './settings_page';
|
import { SettingsPage } from './settings_page';
|
||||||
|
import { useKnowledgeBase } from '@kbn/ai-assistant';
|
||||||
|
|
||||||
|
jest.mock('@kbn/ai-assistant');
|
||||||
|
|
||||||
|
const useKnowledgeBaseMock = useKnowledgeBase as jest.Mock;
|
||||||
|
|
||||||
describe('Settings Page', () => {
|
describe('Settings Page', () => {
|
||||||
|
const appContextValue = {
|
||||||
|
config: { spacesEnabled: true, visibilityEnabled: true, logSourcesEnabled: true },
|
||||||
|
setBreadcrumbs: () => {},
|
||||||
|
};
|
||||||
|
useKnowledgeBaseMock.mockReturnValue({
|
||||||
|
status: {
|
||||||
|
value: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
it('should navigate to home when not authorized', () => {
|
it('should navigate to home when not authorized', () => {
|
||||||
render(<SettingsPage />, {
|
render(<SettingsPage />, {
|
||||||
coreStart: {
|
coreStart: {
|
||||||
|
@ -21,13 +37,16 @@ describe('Settings Page', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
appContextValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(coreStartMock.application.navigateToApp).toBeCalledWith('home');
|
expect(coreStartMock.application.navigateToApp).toBeCalledWith('home');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render settings and knowledge base tabs', () => {
|
it('should render settings and knowledge base tabs', () => {
|
||||||
const { getByTestId } = render(<SettingsPage />);
|
const { getByTestId } = render(<SettingsPage />, {
|
||||||
|
appContextValue,
|
||||||
|
});
|
||||||
|
|
||||||
expect(getByTestId('settingsPageTab-settings')).toBeInTheDocument();
|
expect(getByTestId('settingsPageTab-settings')).toBeInTheDocument();
|
||||||
expect(getByTestId('settingsPageTab-knowledge_base')).toBeInTheDocument();
|
expect(getByTestId('settingsPageTab-knowledge_base')).toBeInTheDocument();
|
||||||
|
@ -36,7 +55,7 @@ describe('Settings Page', () => {
|
||||||
it('should set breadcrumbs', () => {
|
it('should set breadcrumbs', () => {
|
||||||
const setBreadcrumbs = jest.fn();
|
const setBreadcrumbs = jest.fn();
|
||||||
render(<SettingsPage />, {
|
render(<SettingsPage />, {
|
||||||
appContextValue: { setBreadcrumbs },
|
appContextValue: { ...appContextValue, setBreadcrumbs },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(setBreadcrumbs).toHaveBeenCalledWith([
|
expect(setBreadcrumbs).toHaveBeenCalledWith([
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
|
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
|
||||||
|
import { useKnowledgeBase } from '@kbn/ai-assistant';
|
||||||
import { useAppContext } from '../../hooks/use_app_context';
|
import { useAppContext } from '../../hooks/use_app_context';
|
||||||
import { SettingsTab } from './settings_tab/settings_tab';
|
import { SettingsTab } from './settings_tab/settings_tab';
|
||||||
import { KnowledgeBaseTab } from './knowledge_base_tab';
|
import { KnowledgeBaseTab } from './knowledge_base_tab';
|
||||||
|
@ -28,6 +29,7 @@ export function SettingsPage() {
|
||||||
} = useKibana();
|
} = useKibana();
|
||||||
|
|
||||||
const router = useObservabilityAIAssistantManagementRouter();
|
const router = useObservabilityAIAssistantManagementRouter();
|
||||||
|
const knowledgeBase = useKnowledgeBase();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query: { tab },
|
query: { tab },
|
||||||
|
@ -85,6 +87,7 @@ export function SettingsPage() {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
content: <KnowledgeBaseTab />,
|
content: <KnowledgeBaseTab />,
|
||||||
|
disabled: !knowledgeBase.status.value?.enabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'search_connector',
|
id: 'search_connector',
|
||||||
|
|
|
@ -9,10 +9,14 @@ import React from 'react';
|
||||||
import { fireEvent } from '@testing-library/react';
|
import { fireEvent } from '@testing-library/react';
|
||||||
import { render } from '../../../helpers/test_helper';
|
import { render } from '../../../helpers/test_helper';
|
||||||
import { SettingsTab } from './settings_tab';
|
import { SettingsTab } from './settings_tab';
|
||||||
|
import { useAppContext } from '../../../hooks/use_app_context';
|
||||||
|
|
||||||
jest.mock('../../../hooks/use_app_context');
|
jest.mock('../../../hooks/use_app_context');
|
||||||
|
|
||||||
|
const useAppContextMock = useAppContext as jest.Mock;
|
||||||
|
|
||||||
describe('SettingsTab', () => {
|
describe('SettingsTab', () => {
|
||||||
|
useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } });
|
||||||
it('should offer a way to configure Observability AI Assistant visibility in apps', () => {
|
it('should offer a way to configure Observability AI Assistant visibility in apps', () => {
|
||||||
const navigateToAppMock = jest.fn(() => Promise.resolve());
|
const navigateToAppMock = jest.fn(() => Promise.resolve());
|
||||||
const { getByTestId } = render(<SettingsTab />, {
|
const { getByTestId } = render(<SettingsTab />, {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui';
|
import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { useAppContext } from '../../../hooks/use_app_context';
|
||||||
import { useKibana } from '../../../hooks/use_kibana';
|
import { useKibana } from '../../../hooks/use_kibana';
|
||||||
import { UISettings } from './ui_settings';
|
import { UISettings } from './ui_settings';
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ export function SettingsTab() {
|
||||||
const {
|
const {
|
||||||
application: { navigateToApp },
|
application: { navigateToApp },
|
||||||
} = useKibana().services;
|
} = useKibana().services;
|
||||||
|
const { config } = useAppContext();
|
||||||
|
|
||||||
const handleNavigateToConnectors = () => {
|
const handleNavigateToConnectors = () => {
|
||||||
navigateToApp('management', {
|
navigateToApp('management', {
|
||||||
|
@ -30,44 +32,46 @@ export function SettingsTab() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPanel hasBorder grow={false}>
|
<EuiPanel hasBorder grow={false}>
|
||||||
<EuiDescribedFormGroup
|
{config.spacesEnabled && (
|
||||||
fullWidth
|
<EuiDescribedFormGroup
|
||||||
title={
|
fullWidth
|
||||||
<h3>
|
title={
|
||||||
{i18n.translate(
|
<h3>
|
||||||
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel',
|
{i18n.translate(
|
||||||
{
|
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel',
|
||||||
defaultMessage:
|
{
|
||||||
'Show AI Assistant button and Contextual Insights in Observability apps',
|
defaultMessage:
|
||||||
}
|
'Show AI Assistant button and Contextual Insights in Observability apps',
|
||||||
)}
|
}
|
||||||
</h3>
|
)}
|
||||||
}
|
</h3>
|
||||||
description={
|
}
|
||||||
<p>
|
description={
|
||||||
{i18n.translate(
|
<p>
|
||||||
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel',
|
{i18n.translate(
|
||||||
{
|
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantDescriptionLabel',
|
||||||
defaultMessage:
|
{
|
||||||
'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > <your space> > Features.',
|
defaultMessage:
|
||||||
ignoreTag: true,
|
'Toggle the AI Assistant button and Contextual Insights on or off in Observability apps by checking or unchecking the AI Assistant feature in Spaces > <your space> > Features.',
|
||||||
}
|
ignoreTag: true,
|
||||||
)}
|
}
|
||||||
</p>
|
)}
|
||||||
}
|
</p>
|
||||||
>
|
}
|
||||||
<EuiFormRow fullWidth>
|
>
|
||||||
<EuiButton
|
<EuiFormRow fullWidth>
|
||||||
data-test-subj="settingsTabGoToSpacesButton"
|
<EuiButton
|
||||||
onClick={handleNavigateToSpacesConfiguration}
|
data-test-subj="settingsTabGoToSpacesButton"
|
||||||
>
|
onClick={handleNavigateToSpacesConfiguration}
|
||||||
{i18n.translate(
|
>
|
||||||
'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
|
{i18n.translate(
|
||||||
{ defaultMessage: 'Go to Spaces' }
|
'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
|
||||||
)}
|
{ defaultMessage: 'Go to Spaces' }
|
||||||
</EuiButton>
|
)}
|
||||||
</EuiFormRow>
|
</EuiButton>
|
||||||
</EuiDescribedFormGroup>
|
</EuiFormRow>
|
||||||
|
</EuiDescribedFormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
<EuiDescribedFormGroup
|
<EuiDescribedFormGroup
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
@ -18,14 +18,10 @@ import { EuiSpacer } from '@elastic/eui';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty } from 'lodash';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { LogSourcesSettingSynchronisationInfo } from '@kbn/logs-data-access-plugin/public';
|
import { LogSourcesSettingSynchronisationInfo } from '@kbn/logs-data-access-plugin/public';
|
||||||
|
import { useKnowledgeBase } from '@kbn/ai-assistant';
|
||||||
|
import { useAppContext } from '../../../hooks/use_app_context';
|
||||||
import { useKibana } from '../../../hooks/use_kibana';
|
import { useKibana } from '../../../hooks/use_kibana';
|
||||||
|
|
||||||
const settingsKeys = [
|
|
||||||
aiAssistantSimulatedFunctionCalling,
|
|
||||||
aiAssistantSearchConnectorIndexPattern,
|
|
||||||
aiAssistantPreferredAIAssistantType,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function UISettings() {
|
export function UISettings() {
|
||||||
const {
|
const {
|
||||||
docLinks,
|
docLinks,
|
||||||
|
@ -33,6 +29,14 @@ export function UISettings() {
|
||||||
notifications,
|
notifications,
|
||||||
application: { capabilities, getUrlForApp },
|
application: { capabilities, getUrlForApp },
|
||||||
} = useKibana().services;
|
} = useKibana().services;
|
||||||
|
const knowledgeBase = useKnowledgeBase();
|
||||||
|
const { config } = useAppContext();
|
||||||
|
|
||||||
|
const settingsKeys = [
|
||||||
|
aiAssistantSimulatedFunctionCalling,
|
||||||
|
...(knowledgeBase.status.value?.enabled ? [aiAssistantSearchConnectorIndexPattern] : []),
|
||||||
|
...(config.visibilityEnabled ? [aiAssistantPreferredAIAssistantType] : []),
|
||||||
|
];
|
||||||
|
|
||||||
const { fields, handleFieldChange, unsavedChanges, saveAll, isSaving, cleanUnsavedChanges } =
|
const { fields, handleFieldChange, unsavedChanges, saveAll, isSaving, cleanUnsavedChanges } =
|
||||||
useEditableSettings(settingsKeys);
|
useEditableSettings(settingsKeys);
|
||||||
|
@ -84,12 +88,13 @@ export function UISettings() {
|
||||||
</FieldRowProvider>
|
</FieldRowProvider>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{config.logSourcesEnabled && (
|
||||||
<LogSourcesSettingSynchronisationInfo
|
<LogSourcesSettingSynchronisationInfo
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
logSourcesValue={settings.client.get(aiAssistantLogsIndexPattern)}
|
logSourcesValue={settings.client.get(aiAssistantLogsIndexPattern)}
|
||||||
getUrlForApp={getUrlForApp}
|
getUrlForApp={getUrlForApp}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isEmpty(unsavedChanges) && (
|
{!isEmpty(unsavedChanges) && (
|
||||||
<BottomBarActions
|
<BottomBarActions
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 { schema, type TypeOf } from '@kbn/config-schema';
|
||||||
|
import { PluginConfigDescriptor } from '@kbn/core-plugins-server';
|
||||||
|
|
||||||
|
const configSchema = schema.object({
|
||||||
|
visibilityEnabled: schema.boolean({ defaultValue: true }),
|
||||||
|
spacesEnabled: schema.boolean({ defaultValue: true }),
|
||||||
|
logSourcesEnabled: schema.boolean({ defaultValue: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ObservabilityAIAssistantManagementConfig = TypeOf<typeof configSchema>;
|
||||||
|
|
||||||
|
export const config: PluginConfigDescriptor<ObservabilityAIAssistantManagementConfig> = {
|
||||||
|
schema: configSchema,
|
||||||
|
exposeToBrowser: { logSourcesEnabled: true, spacesEnabled: true, visibilityEnabled: true },
|
||||||
|
};
|
|
@ -5,6 +5,8 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export { config } from './config';
|
||||||
|
|
||||||
export const plugin = async () => {
|
export const plugin = async () => {
|
||||||
const { AiAssistantManagementPlugin } = await import('./plugin');
|
const { AiAssistantManagementPlugin } = await import('./plugin');
|
||||||
return new AiAssistantManagementPlugin();
|
return new AiAssistantManagementPlugin();
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "target/types"
|
"outDir": "target/types"
|
||||||
},
|
},
|
||||||
"include": ["common/**/*", "public/**/*", "server/**/*"],
|
"include": [
|
||||||
|
"common/**/*",
|
||||||
|
"public/**/*",
|
||||||
|
"server/**/*"
|
||||||
|
],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/core",
|
"@kbn/core",
|
||||||
"@kbn/home-plugin",
|
"@kbn/home-plugin",
|
||||||
|
@ -22,6 +26,11 @@
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
"@kbn/core-ui-settings-common",
|
"@kbn/core-ui-settings-common",
|
||||||
"@kbn/logs-data-access-plugin",
|
"@kbn/logs-data-access-plugin",
|
||||||
|
"@kbn/core-plugins-browser",
|
||||||
|
"@kbn/ai-assistant",
|
||||||
|
"@kbn/core-plugins-server"
|
||||||
],
|
],
|
||||||
"exclude": ["target/**/*"]
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
"actions",
|
"actions",
|
||||||
"licensing",
|
"licensing",
|
||||||
"observabilityAIAssistant",
|
"observabilityAIAssistant",
|
||||||
"observabilityAIAssistantApp",
|
|
||||||
"triggersActionsUi",
|
"triggersActionsUi",
|
||||||
"share"
|
"share"
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
|
|
||||||
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|
||||||
import { I18nProvider } from '@kbn/i18n-react';
|
|
||||||
import type { SearchAssistantPluginStartDependencies } from './types';
|
|
||||||
import { SearchAssistantRouter } from './components/routes/router';
|
|
||||||
|
|
||||||
export const renderApp = (
|
|
||||||
core: CoreStart,
|
|
||||||
services: SearchAssistantPluginStartDependencies,
|
|
||||||
appMountParameters: AppMountParameters
|
|
||||||
) => {
|
|
||||||
ReactDOM.render(
|
|
||||||
<KibanaRenderContextProvider {...core}>
|
|
||||||
<KibanaContextProvider services={{ ...core, ...services }}>
|
|
||||||
<I18nProvider>
|
|
||||||
<SearchAssistantRouter history={appMountParameters.history} />
|
|
||||||
</I18nProvider>
|
|
||||||
</KibanaContextProvider>
|
|
||||||
</KibanaRenderContextProvider>,
|
|
||||||
appMountParameters.element
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => ReactDOM.unmountComponentAtNode(appMountParameters.element);
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
|
|
||||||
export const App: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<KibanaPageTemplate.Section alignment="top" restrictWidth={false} grow paddingSize="none">
|
|
||||||
<div />
|
|
||||||
</KibanaPageTemplate.Section>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { AssistantAvatar, useAbortableAsync } from '@kbn/observability-ai-assistant-plugin/public';
|
||||||
|
import { EuiButton, EuiLoadingSpinner, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { useAIAssistantAppService, ChatFlyout } from '@kbn/ai-assistant';
|
||||||
|
import { useKibana } from '@kbn/ai-assistant/src/hooks/use_kibana';
|
||||||
|
import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types';
|
||||||
|
import { EuiErrorBoundary } from '@elastic/eui';
|
||||||
|
import type { CoreStart } from '@kbn/core/public';
|
||||||
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme';
|
||||||
|
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||||
|
|
||||||
|
interface NavControlWithProviderDeps {
|
||||||
|
coreStart: CoreStart;
|
||||||
|
pluginsStart: AIAssistantPluginStartDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavControlWithProvider = ({ coreStart, pluginsStart }: NavControlWithProviderDeps) => {
|
||||||
|
return (
|
||||||
|
<EuiErrorBoundary>
|
||||||
|
<KibanaThemeProvider theme={coreStart.theme}>
|
||||||
|
<KibanaContextProvider
|
||||||
|
services={{
|
||||||
|
...coreStart,
|
||||||
|
...pluginsStart,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RedirectAppLinks coreStart={coreStart}>
|
||||||
|
<coreStart.i18n.Context>
|
||||||
|
<NavControl />
|
||||||
|
</coreStart.i18n.Context>
|
||||||
|
</RedirectAppLinks>
|
||||||
|
</KibanaContextProvider>
|
||||||
|
</KibanaThemeProvider>
|
||||||
|
</EuiErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NavControl() {
|
||||||
|
const service = useAIAssistantAppService();
|
||||||
|
|
||||||
|
const {
|
||||||
|
services: { notifications, observabilityAIAssistant },
|
||||||
|
} = useKibana();
|
||||||
|
|
||||||
|
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
||||||
|
|
||||||
|
const chatService = useAbortableAsync(
|
||||||
|
({ signal }) => {
|
||||||
|
return hasBeenOpened
|
||||||
|
? service.start({ signal }).catch((error) => {
|
||||||
|
notifications?.toasts.addError(error, {
|
||||||
|
title: i18n.translate('xpack.searchAssistant.navControl.initFailureErrorTitle', {
|
||||||
|
defaultMessage: 'Failed to initialize AI Assistant',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
setHasBeenOpened(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
},
|
||||||
|
[service, hasBeenOpened, notifications?.toasts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const keyRef = useRef(v4());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const conversationSubscription = service.conversations.predefinedConversation$.subscribe(() => {
|
||||||
|
keyRef.current = v4();
|
||||||
|
setHasBeenOpened(true);
|
||||||
|
setIsOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
conversationSubscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [service.conversations.predefinedConversation$]);
|
||||||
|
|
||||||
|
const { messages, title } = useObservable(service.conversations.predefinedConversation$) ?? {
|
||||||
|
messages: [],
|
||||||
|
title: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = useEuiTheme().euiTheme;
|
||||||
|
|
||||||
|
const buttonCss = css`
|
||||||
|
padding: 0px 8px;
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
fill: ${theme.colors.darkestShade};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiToolTip content={buttonLabel}>
|
||||||
|
<EuiButton
|
||||||
|
aria-label={buttonLabel}
|
||||||
|
data-test-subj="AiAssistantAppNavControlButton"
|
||||||
|
css={buttonCss}
|
||||||
|
onClick={() => {
|
||||||
|
service.conversations.openNewConversation({
|
||||||
|
messages: [],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
size="s"
|
||||||
|
fullWidth={false}
|
||||||
|
minWidth={0}
|
||||||
|
>
|
||||||
|
{chatService.loading ? <EuiLoadingSpinner size="s" /> : <AssistantAvatar size="xs" />}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiToolTip>
|
||||||
|
{chatService.value &&
|
||||||
|
Boolean(observabilityAIAssistant?.ObservabilityAIAssistantChatServiceContext) ? (
|
||||||
|
<observabilityAIAssistant.ObservabilityAIAssistantChatServiceContext.Provider
|
||||||
|
value={chatService.value}
|
||||||
|
>
|
||||||
|
<ChatFlyout
|
||||||
|
key={keyRef.current}
|
||||||
|
isOpen={isOpen}
|
||||||
|
initialMessages={messages}
|
||||||
|
initialTitle={title ?? ''}
|
||||||
|
onClose={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</observabilityAIAssistant.ObservabilityAIAssistantChatServiceContext.Provider>
|
||||||
|
) : undefined}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonLabel = i18n.translate(
|
||||||
|
'xpack.searchAssistant.navControl.openTheAIAssistantPopoverLabel',
|
||||||
|
{ defaultMessage: 'Open the AI Assistant' }
|
||||||
|
);
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 { dynamic } from '@kbn/shared-ux-utility';
|
||||||
|
import React from 'react';
|
||||||
|
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||||
|
import { AIAssistantAppService } from '@kbn/ai-assistant';
|
||||||
|
import { AIAssistantPluginStartDependencies } from '@kbn/ai-assistant/src/types';
|
||||||
|
|
||||||
|
const LazyNavControlWithProvider = dynamic(() =>
|
||||||
|
import('.').then((m) => ({ default: m.NavControlWithProvider }))
|
||||||
|
);
|
||||||
|
|
||||||
|
interface NavControlInitiatorProps {
|
||||||
|
appService: AIAssistantAppService;
|
||||||
|
coreStart: CoreStart;
|
||||||
|
pluginsStart: AIAssistantPluginStartDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavControlInitiator = ({ coreStart, pluginsStart }: NavControlInitiatorProps) => {
|
||||||
|
return <LazyNavControlWithProvider coreStart={coreStart} pluginsStart={pluginsStart} />;
|
||||||
|
};
|
|
@ -1,36 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { ConversationView } from '@kbn/ai-assistant';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|
||||||
|
|
||||||
export function ConversationViewWithProps() {
|
|
||||||
const { conversationId } = useParams<{ conversationId?: string }>();
|
|
||||||
const {
|
|
||||||
services: { application, http },
|
|
||||||
} = useKibana();
|
|
||||||
function navigateToConversation(nextConversationId?: string) {
|
|
||||||
application?.navigateToUrl(
|
|
||||||
http?.basePath.prepend(`/app/searchAssistant/conversations/${nextConversationId || ''}`) || ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ConversationView
|
|
||||||
conversationId={conversationId}
|
|
||||||
navigateToConversation={navigateToConversation}
|
|
||||||
newConversationHref={
|
|
||||||
http?.basePath.prepend(`/app/searchAssistant/conversations/new|| ''}`) || ''
|
|
||||||
}
|
|
||||||
getConversationHref={(id: string) =>
|
|
||||||
http?.basePath.prepend(`/app/searchAssistant/conversations/${id || ''}`) || ''
|
|
||||||
}
|
|
||||||
scopes={['search']}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
||||||
* or more contributor license agreements. Licensed under the Elastic License
|
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
||||||
* 2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { History } from 'history';
|
|
||||||
import { Route, Router, Routes } from '@kbn/shared-ux-router';
|
|
||||||
import { Redirect } from 'react-router-dom';
|
|
||||||
import { SearchAIAssistantPageTemplate } from '../page_template';
|
|
||||||
import { ConversationViewWithProps } from './conversations/conversation_view_with_props';
|
|
||||||
|
|
||||||
export const SearchAssistantRouter: React.FC<{ history: History }> = ({ history }) => {
|
|
||||||
return (
|
|
||||||
<SearchAIAssistantPageTemplate>
|
|
||||||
<Router history={history}>
|
|
||||||
<Routes>
|
|
||||||
<Redirect from="/" to="/conversations/new" exact />
|
|
||||||
<Redirect from="/conversations" to="/conversations/new" exact />
|
|
||||||
<Route path="/conversations/new" exact>
|
|
||||||
<ConversationViewWithProps />
|
|
||||||
</Route>
|
|
||||||
<Route path="/conversations/:conversationId">
|
|
||||||
<ConversationViewWithProps />
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</SearchAIAssistantPageTemplate>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -5,20 +5,16 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public';
|
||||||
DEFAULT_APP_CATEGORIES,
|
import { createAppService } from '@kbn/ai-assistant';
|
||||||
type CoreSetup,
|
import ReactDOM from 'react-dom';
|
||||||
type Plugin,
|
import React from 'react';
|
||||||
CoreStart,
|
|
||||||
AppMountParameters,
|
|
||||||
PluginInitializerContext,
|
|
||||||
} from '@kbn/core/public';
|
|
||||||
import { i18n } from '@kbn/i18n';
|
|
||||||
import type {
|
import type {
|
||||||
SearchAssistantPluginSetup,
|
SearchAssistantPluginSetup,
|
||||||
SearchAssistantPluginStart,
|
SearchAssistantPluginStart,
|
||||||
SearchAssistantPluginStartDependencies,
|
SearchAssistantPluginStartDependencies,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { NavControlInitiator } from './components/nav_control/lazy_nav_control';
|
||||||
|
|
||||||
export interface PublicConfigType {
|
export interface PublicConfigType {
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -44,36 +40,43 @@ export class SearchAssistantPlugin
|
||||||
public setup(
|
public setup(
|
||||||
core: CoreSetup<SearchAssistantPluginStartDependencies, SearchAssistantPluginStart>
|
core: CoreSetup<SearchAssistantPluginStartDependencies, SearchAssistantPluginStart>
|
||||||
): SearchAssistantPluginSetup {
|
): SearchAssistantPluginSetup {
|
||||||
if (!this.config.ui.enabled) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
core.application.register({
|
|
||||||
id: 'searchAssistant',
|
|
||||||
title: i18n.translate('xpack.searchAssistant.appTitle', {
|
|
||||||
defaultMessage: 'Search Assistant',
|
|
||||||
}),
|
|
||||||
euiIconType: 'logoEnterpriseSearch',
|
|
||||||
appRoute: '/app/searchAssistant',
|
|
||||||
category: DEFAULT_APP_CATEGORIES.search,
|
|
||||||
visibleIn: [],
|
|
||||||
deepLinks: [],
|
|
||||||
mount: async (appMountParameters: AppMountParameters<unknown>) => {
|
|
||||||
// Load application bundle and Get start services
|
|
||||||
const [{ renderApp }, [coreStart, pluginsStart]] = await Promise.all([
|
|
||||||
import('./application'),
|
|
||||||
core.getStartServices() as Promise<
|
|
||||||
[CoreStart, SearchAssistantPluginStartDependencies, unknown]
|
|
||||||
>,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return renderApp(coreStart, pluginsStart, appMountParameters);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): SearchAssistantPluginStart {
|
public start(
|
||||||
|
coreStart: CoreStart,
|
||||||
|
pluginsStart: SearchAssistantPluginStartDependencies
|
||||||
|
): SearchAssistantPluginStart {
|
||||||
|
if (!this.config.ui.enabled) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const appService = createAppService({
|
||||||
|
pluginsStart,
|
||||||
|
});
|
||||||
|
const isEnabled = appService.isEnabled();
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreStart.chrome.navControls.registerRight({
|
||||||
|
mount: (element) => {
|
||||||
|
ReactDOM.render(
|
||||||
|
<NavControlInitiator
|
||||||
|
appService={appService}
|
||||||
|
coreStart={coreStart}
|
||||||
|
pluginsStart={pluginsStart}
|
||||||
|
/>,
|
||||||
|
element,
|
||||||
|
() => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
// right before the user profile
|
||||||
|
order: 1001,
|
||||||
|
});
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
|
|
||||||
import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
import { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||||
import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
|
import { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public';
|
||||||
|
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||||
|
import { MlPluginStart } from '@kbn/ml-plugin/public';
|
||||||
|
import { SharePluginStart } from '@kbn/share-plugin/public';
|
||||||
|
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
export interface SearchAssistantPluginSetup {}
|
export interface SearchAssistantPluginSetup {}
|
||||||
|
@ -15,6 +19,10 @@ export interface SearchAssistantPluginSetup {}
|
||||||
export interface SearchAssistantPluginStart {}
|
export interface SearchAssistantPluginStart {}
|
||||||
|
|
||||||
export interface SearchAssistantPluginStartDependencies {
|
export interface SearchAssistantPluginStartDependencies {
|
||||||
|
licensing: LicensingPluginStart;
|
||||||
|
ml: MlPluginStart;
|
||||||
observabilityAIAssistant: ObservabilityAIAssistantPublicStart;
|
observabilityAIAssistant: ObservabilityAIAssistantPublicStart;
|
||||||
|
share: SharePluginStart;
|
||||||
|
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||||
usageCollection?: UsageCollectionStart;
|
usageCollection?: UsageCollectionStart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,17 +13,22 @@
|
||||||
],
|
],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/core",
|
"@kbn/core",
|
||||||
"@kbn/react-kibana-context-render",
|
|
||||||
"@kbn/kibana-react-plugin",
|
"@kbn/kibana-react-plugin",
|
||||||
"@kbn/i18n-react",
|
|
||||||
"@kbn/shared-ux-page-kibana-template",
|
"@kbn/shared-ux-page-kibana-template",
|
||||||
"@kbn/usage-collection-plugin",
|
"@kbn/usage-collection-plugin",
|
||||||
"@kbn/observability-ai-assistant-plugin",
|
"@kbn/observability-ai-assistant-plugin",
|
||||||
"@kbn/config-schema",
|
"@kbn/config-schema",
|
||||||
"@kbn/ai-assistant",
|
"@kbn/ai-assistant",
|
||||||
"@kbn/i18n",
|
"@kbn/i18n",
|
||||||
"@kbn/shared-ux-router",
|
"@kbn/serverless",
|
||||||
"@kbn/serverless"
|
"@kbn/react-kibana-context-theme",
|
||||||
|
"@kbn/shared-ux-link-redirect-app",
|
||||||
|
"@kbn/shared-ux-utility",
|
||||||
|
"@kbn/core-lifecycle-browser",
|
||||||
|
"@kbn/licensing-plugin",
|
||||||
|
"@kbn/ml-plugin",
|
||||||
|
"@kbn/share-plugin",
|
||||||
|
"@kbn/triggers-actions-ui-plugin"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"target/**/*",
|
"target/**/*",
|
||||||
|
|
|
@ -13,12 +13,12 @@
|
||||||
"requiredPlugins": [
|
"requiredPlugins": [
|
||||||
"actions",
|
"actions",
|
||||||
"features",
|
"features",
|
||||||
|
"ml",
|
||||||
"share",
|
"share",
|
||||||
],
|
],
|
||||||
"optionalPlugins": [
|
"optionalPlugins": [
|
||||||
"cloud",
|
"cloud",
|
||||||
"console",
|
"console",
|
||||||
"ml"
|
|
||||||
],
|
],
|
||||||
"requiredBundles": [
|
"requiredBundles": [
|
||||||
"kibanaReact"
|
"kibanaReact"
|
||||||
|
|
|
@ -49,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
expect(res.body).to.eql({
|
expect(res.body).to.eql({
|
||||||
ready: false,
|
ready: false,
|
||||||
model_name: TINY_ELSER.id,
|
model_name: TINY_ELSER.id,
|
||||||
|
enabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -69,6 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
||||||
expect(res.body).to.eql({
|
expect(res.body).to.eql({
|
||||||
ready: false,
|
ready: false,
|
||||||
model_name: TINY_ELSER.id,
|
model_name: TINY_ELSER.id,
|
||||||
|
enabled: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,5 +64,8 @@ export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) {
|
||||||
keyName
|
keyName
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
async expectAIAssistantToExist() {
|
||||||
|
await testSubjects.existOrFail('AiAssistantAppNavControlButton');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,5 +84,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||||
await pageObjects.svlSearchHomePage.createApiKeyInFlyout('ftr-test-key');
|
await pageObjects.svlSearchHomePage.createApiKeyInFlyout('ftr-test-key');
|
||||||
await pageObjects.svlSearchHomePage.closeConnectionDetailsFlyout();
|
await pageObjects.svlSearchHomePage.closeConnectionDetailsFlyout();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the AI assistant', async () => {
|
||||||
|
await pageObjects.svlSearchHomePage.expectAIAssistantToExist();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue