mirror of
https://github.com/elastic/kibana.git
synced 2025-04-20 07:48:52 -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.serverless.observability.enabled: false
|
||||
enterpriseSearch.enabled: false
|
||||
xpack.observabilityAIAssistant.enabled: false
|
||||
xpack.osquery.enabled: false
|
||||
|
||||
# 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
|
||||
|
||||
# Semantic text UI
|
||||
|
||||
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
|
||||
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
|
||||
xpack.observabilityAIAssistant.scope: "observability"
|
||||
|
||||
# Specify in telemetry the project type
|
||||
telemetry.labels.serverless: observability
|
||||
|
|
|
@ -362,6 +362,9 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
|
|||
'xpack.observability_onboarding.ui.enabled (boolean?)',
|
||||
'xpack.observabilityLogsExplorer.navigation.showAppLink (boolean?|never)',
|
||||
'xpack.observabilityAIAssistant.scope (observability?|search?)',
|
||||
'xpack.observabilityAiAssistantManagement.logSourcesEnabled (boolean?)',
|
||||
'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)',
|
||||
'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)',
|
||||
'share.new_version.enabled (boolean?)',
|
||||
'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)',
|
||||
/**
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { ConnectorSelectorBase } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import type { UseGenAIConnectorsResult } from '../hooks/use_genai_connectors';
|
||||
import { useKibana } from '../hooks/use_kibana';
|
||||
import { useKnowledgeBase } from '../hooks';
|
||||
|
||||
export function ChatActionsMenu({
|
||||
connectors,
|
||||
|
@ -31,6 +32,7 @@ export function ChatActionsMenu({
|
|||
onCopyConversationClick: () => void;
|
||||
}) {
|
||||
const { application, http } = useKibana().services;
|
||||
const knowledgeBase = useKnowledgeBase();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleNavigateToConnectors = () => {
|
||||
|
@ -91,15 +93,19 @@ export function ChatActionsMenu({
|
|||
defaultMessage: 'Actions',
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', {
|
||||
defaultMessage: 'Manage knowledge base',
|
||||
}),
|
||||
onClick: () => {
|
||||
toggleActionsMenu();
|
||||
handleNavigateToSettingsKnowledgeBase();
|
||||
},
|
||||
},
|
||||
...(knowledgeBase?.status.value?.enabled
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.knowledgeBase', {
|
||||
defaultMessage: 'Manage knowledge base',
|
||||
}),
|
||||
onClick: () => {
|
||||
toggleActionsMenu();
|
||||
handleNavigateToSettingsKnowledgeBase();
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: i18n.translate('xpack.aiAssistant.chatHeader.actions.settings', {
|
||||
defaultMessage: 'AI Assistant Settings',
|
||||
|
|
|
@ -37,6 +37,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
|
|||
loading: false,
|
||||
value: {
|
||||
ready: true,
|
||||
enabled: true,
|
||||
},
|
||||
refresh: () => {},
|
||||
},
|
||||
|
|
|
@ -123,7 +123,7 @@ export function ChatBody({
|
|||
showLinkToConversationsApp: boolean;
|
||||
onConversationUpdate: (conversation: { conversation: Conversation['conversation'] }) => void;
|
||||
onToggleFlyoutPositionMode?: (flyoutPositionMode: FlyoutPositionMode) => void;
|
||||
navigateToConversation: (conversationId?: string) => void;
|
||||
navigateToConversation?: (conversationId?: string) => void;
|
||||
}) {
|
||||
const license = useLicense();
|
||||
const hasCorrectLicense = license?.hasAtLeast('enterprise');
|
||||
|
|
|
@ -53,7 +53,7 @@ export function ChatFlyout({
|
|||
initialFlyoutPositionMode?: FlyoutPositionMode;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
navigateToConversation(conversationId?: string): void;
|
||||
navigateToConversation?: (conversationId?: string) => void;
|
||||
}) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const breakpoint = useCurrentEuiBreakpoint();
|
||||
|
@ -272,10 +272,14 @@ export function ChatFlyout({
|
|||
conversationList.conversations.refresh();
|
||||
}}
|
||||
onToggleFlyoutPositionMode={handleToggleFlyoutPositionMode}
|
||||
navigateToConversation={(newConversationId?: string) => {
|
||||
if (onClose) onClose();
|
||||
navigateToConversation(newConversationId);
|
||||
}}
|
||||
navigateToConversation={
|
||||
navigateToConversation
|
||||
? (newConversationId?: string) => {
|
||||
if (onClose) onClose();
|
||||
navigateToConversation(newConversationId);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ export function ChatHeader({
|
|||
onCopyConversation: () => void;
|
||||
onSaveTitle: (title: string) => void;
|
||||
onToggleFlyoutPositionMode?: (newFlyoutPositionMode: FlyoutPositionMode) => void;
|
||||
navigateToConversation: (nextConversationId?: string) => void;
|
||||
navigateToConversation?: (nextConversationId?: string) => void;
|
||||
}) {
|
||||
const theme = useEuiTheme();
|
||||
const breakpoint = useCurrentEuiBreakpoint();
|
||||
|
@ -164,31 +164,32 @@ export function ChatHeader({
|
|||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
anchorPosition="downLeft"
|
||||
button={
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel',
|
||||
{ defaultMessage: 'Navigate to conversations' }
|
||||
)}
|
||||
display="block"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel',
|
||||
{navigateToConversation ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
anchorPosition="downLeft"
|
||||
button={
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.aiAssistant.chatHeader.euiToolTip.navigateToConversationsLabel',
|
||||
{ defaultMessage: 'Navigate to conversations' }
|
||||
)}
|
||||
data-test-subj="observabilityAiAssistantChatHeaderButton"
|
||||
iconType="discuss"
|
||||
onClick={() => navigateToConversation(conversationId)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
display="block"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.aiAssistant.chatHeader.euiButtonIcon.navigateToConversationsLabel',
|
||||
{ defaultMessage: 'Navigate to conversations' }
|
||||
)}
|
||||
data-test-subj="observabilityAiAssistantChatHeaderButton"
|
||||
iconType="discuss"
|
||||
onClick={() => navigateToConversation(conversationId)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ const defaultProps: ComponentProps<typeof Component> = {
|
|||
loading: false,
|
||||
value: {
|
||||
ready: true,
|
||||
enabled: true,
|
||||
},
|
||||
refresh: () => {},
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ const defaultProps: ComponentStoryObj<typeof Component> = {
|
|||
loading: false,
|
||||
value: {
|
||||
ready: false,
|
||||
enabled: true,
|
||||
},
|
||||
refresh: () => {},
|
||||
},
|
||||
|
@ -43,12 +44,15 @@ export const Loading: ComponentStoryObj<typeof Component> = merge({}, defaultPro
|
|||
});
|
||||
|
||||
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, {
|
||||
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: {
|
||||
status: {
|
||||
loading: false,
|
||||
value: { ready: false },
|
||||
value: { ready: false, enabled: true },
|
||||
},
|
||||
isInstalling: false,
|
||||
installError: new Error(),
|
||||
|
@ -66,5 +70,5 @@ export const InstallError: ComponentStoryObj<typeof Component> = merge({}, defau
|
|||
});
|
||||
|
||||
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}
|
||||
onSetupConnectorClick={handleConnectorClick}
|
||||
/>
|
||||
|
||||
<WelcomeMessageKnowledgeBase connectors={connectors} knowledgeBase={knowledgeBase} />
|
||||
{knowledgeBase.status.value?.enabled ? (
|
||||
<WelcomeMessageKnowledgeBase connectors={connectors} knowledgeBase={knowledgeBase} />
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
|
|
|
@ -25,7 +25,7 @@ const SECOND_SLOT_CONTAINER_WIDTH = 400;
|
|||
|
||||
interface ConversationViewProps {
|
||||
conversationId?: string;
|
||||
navigateToConversation: (nextConversationId?: string) => void;
|
||||
navigateToConversation?: (nextConversationId?: string) => void;
|
||||
getConversationHref?: (conversationId: string) => string;
|
||||
newConversationHref?: string;
|
||||
scopes?: AssistantScope[];
|
||||
|
@ -81,7 +81,9 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
|
|||
const handleConversationUpdate = (conversation: { conversation: { id: string } }) => {
|
||||
if (!conversationId) {
|
||||
updateConversationIdInPlace(conversation.conversation.id);
|
||||
navigateToConversation(conversation.conversation.id);
|
||||
if (navigateToConversation) {
|
||||
navigateToConversation(conversation.conversation.id);
|
||||
}
|
||||
}
|
||||
handleRefreshConversations();
|
||||
};
|
||||
|
@ -143,7 +145,7 @@ export const ConversationView: React.FC<ConversationViewProps> = ({
|
|||
isLoading={conversationList.isLoading}
|
||||
onConversationDeleteClick={(deletedConversationId) => {
|
||||
conversationList.deleteConversation(deletedConversationId).then(() => {
|
||||
if (deletedConversationId === conversationId) {
|
||||
if (deletedConversationId === conversationId && navigateToConversation) {
|
||||
navigateToConversation(undefined);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ export function useKnowledgeBase(): UseKnowledgeBaseResult {
|
|||
error: undefined,
|
||||
value: {
|
||||
ready: true,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ import { useAIAssistantAppService } from './use_ai_assistant_app_service';
|
|||
export interface UseKnowledgeBaseResult {
|
||||
status: AbortableAsyncState<{
|
||||
ready: boolean;
|
||||
enabled: boolean;
|
||||
error?: any;
|
||||
deployment_state?: MlDeploymentState;
|
||||
allocation_state?: MlDeploymentAllocationState;
|
||||
|
|
|
@ -11,6 +11,7 @@ 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')])),
|
||||
enableKnowledgeBase: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
|
||||
export type ObservabilityAIAssistantConfig = TypeOf<typeof config>;
|
||||
|
|
|
@ -159,11 +159,14 @@ export class ObservabilityAIAssistantPlugin
|
|||
core,
|
||||
taskManager: plugins.taskManager,
|
||||
getModelId,
|
||||
enableKnowledgeBase: this.config.enableKnowledgeBase,
|
||||
}));
|
||||
|
||||
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({
|
||||
core,
|
||||
|
|
|
@ -28,6 +28,7 @@ const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({
|
|||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
enabled: boolean;
|
||||
ready: boolean;
|
||||
error?: any;
|
||||
deployment_state?: MlDeploymentState;
|
||||
|
|
|
@ -707,14 +707,16 @@ export class ObservabilityAIAssistantClient {
|
|||
queries: Array<{ text: string; boost?: number }>;
|
||||
categories?: string[];
|
||||
}): Promise<{ entries: RecalledEntry[] }> => {
|
||||
return this.dependencies.knowledgeBaseService.recall({
|
||||
namespace: this.dependencies.namespace,
|
||||
user: this.dependencies.user,
|
||||
queries,
|
||||
categories,
|
||||
esClient: this.dependencies.esClient,
|
||||
uiSettingsClient: this.dependencies.uiSettingsClient,
|
||||
});
|
||||
return (
|
||||
this.dependencies.knowledgeBaseService?.recall({
|
||||
namespace: this.dependencies.namespace,
|
||||
user: this.dependencies.user,
|
||||
queries,
|
||||
categories,
|
||||
esClient: this.dependencies.esClient,
|
||||
uiSettingsClient: this.dependencies.uiSettingsClient,
|
||||
}) || { entries: [] }
|
||||
);
|
||||
};
|
||||
|
||||
getKnowledgeBaseStatus = () => {
|
||||
|
|
|
@ -70,6 +70,7 @@ export class ObservabilityAIAssistantService {
|
|||
private readonly logger: Logger;
|
||||
private readonly getModelId: () => Promise<string>;
|
||||
private kbService?: KnowledgeBaseService;
|
||||
private enableKnowledgeBase: boolean;
|
||||
|
||||
private readonly registrations: RegistrationCallback[] = [];
|
||||
|
||||
|
@ -78,36 +79,40 @@ export class ObservabilityAIAssistantService {
|
|||
core,
|
||||
taskManager,
|
||||
getModelId,
|
||||
enableKnowledgeBase,
|
||||
}: {
|
||||
logger: Logger;
|
||||
core: CoreSetup<ObservabilityAIAssistantPluginStartDependencies>;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
getModelId: () => Promise<string>;
|
||||
enableKnowledgeBase: boolean;
|
||||
}) {
|
||||
this.core = core;
|
||||
this.logger = logger;
|
||||
this.getModelId = getModelId;
|
||||
this.enableKnowledgeBase = enableKnowledgeBase;
|
||||
|
||||
this.allowInit();
|
||||
|
||||
taskManager.registerTaskDefinitions({
|
||||
[INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: {
|
||||
title: 'Index queued KB articles',
|
||||
description:
|
||||
'Indexes previously registered entries into the knowledge base when it is ready',
|
||||
timeout: '30m',
|
||||
maxAttempts: 2,
|
||||
createTaskRunner: (context) => {
|
||||
return {
|
||||
run: async () => {
|
||||
if (this.kbService) {
|
||||
await this.kbService.processQueue();
|
||||
}
|
||||
},
|
||||
};
|
||||
if (enableKnowledgeBase) {
|
||||
taskManager.registerTaskDefinitions({
|
||||
[INDEX_QUEUED_DOCUMENTS_TASK_TYPE]: {
|
||||
title: 'Index queued KB articles',
|
||||
description:
|
||||
'Indexes previously registered entries into the knowledge base when it is ready',
|
||||
timeout: '30m',
|
||||
maxAttempts: 2,
|
||||
createTaskRunner: (context) => {
|
||||
return {
|
||||
run: async () => {
|
||||
if (this.kbService) {
|
||||
await this.kbService.processQueue();
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getKnowledgeBaseStatus() {
|
||||
|
@ -237,6 +242,7 @@ export class ObservabilityAIAssistantService {
|
|||
esClient,
|
||||
taskManagerStart: pluginsStart.taskManager,
|
||||
getModelId: this.getModelId,
|
||||
enabled: this.enableKnowledgeBase,
|
||||
});
|
||||
|
||||
this.logger.info('Successfully set up index assets');
|
||||
|
@ -331,58 +337,62 @@ export class ObservabilityAIAssistantService {
|
|||
}
|
||||
|
||||
addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void {
|
||||
this.init()
|
||||
.then(() => {
|
||||
this.kbService!.queue(
|
||||
entries.flatMap((entry) => {
|
||||
const entryWithSystemProperties = {
|
||||
...entry,
|
||||
'@timestamp': new Date().toISOString(),
|
||||
doc_id: entry.id,
|
||||
public: true,
|
||||
confidence: 'high' as const,
|
||||
type: 'contextual' as const,
|
||||
is_correction: false,
|
||||
labels: {
|
||||
...entry.labels,
|
||||
},
|
||||
role: KnowledgeBaseEntryRole.Elastic,
|
||||
};
|
||||
if (this.enableKnowledgeBase) {
|
||||
this.init()
|
||||
.then(() => {
|
||||
this.kbService!.queue(
|
||||
entries.flatMap((entry) => {
|
||||
const entryWithSystemProperties = {
|
||||
...entry,
|
||||
'@timestamp': new Date().toISOString(),
|
||||
doc_id: entry.id,
|
||||
public: true,
|
||||
confidence: 'high' as const,
|
||||
type: 'contextual' as const,
|
||||
is_correction: false,
|
||||
labels: {
|
||||
...entry.labels,
|
||||
},
|
||||
role: KnowledgeBaseEntryRole.Elastic,
|
||||
};
|
||||
|
||||
const operations =
|
||||
'texts' in entryWithSystemProperties
|
||||
? splitKbText(entryWithSystemProperties)
|
||||
: [
|
||||
{
|
||||
type: KnowledgeBaseEntryOperationType.Index,
|
||||
document: entryWithSystemProperties,
|
||||
},
|
||||
];
|
||||
const operations =
|
||||
'texts' in entryWithSystemProperties
|
||||
? splitKbText(entryWithSystemProperties)
|
||||
: [
|
||||
{
|
||||
type: KnowledgeBaseEntryOperationType.Index,
|
||||
document: entryWithSystemProperties,
|
||||
},
|
||||
];
|
||||
|
||||
return operations;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(
|
||||
`Could not index ${entries.length} entries because of an initialisation error`
|
||||
);
|
||||
this.logger.error(error);
|
||||
});
|
||||
return operations;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(
|
||||
`Could not index ${entries.length} entries because of an initialisation error`
|
||||
);
|
||||
this.logger.error(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) {
|
||||
this.addToKnowledgeBaseQueue(
|
||||
entries.map((entry) => {
|
||||
return {
|
||||
...entry,
|
||||
labels: {
|
||||
...entry.labels,
|
||||
category: categoryId,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
if (this.enableKnowledgeBase) {
|
||||
this.addToKnowledgeBaseQueue(
|
||||
entries.map((entry) => {
|
||||
return {
|
||||
...entry,
|
||||
labels: {
|
||||
...entry.labels,
|
||||
category: categoryId,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
register(cb: RegistrationCallback) {
|
||||
|
|
|
@ -34,6 +34,7 @@ interface Dependencies {
|
|||
logger: Logger;
|
||||
taskManagerStart: TaskManagerStartContract;
|
||||
getModelId: () => Promise<string>;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface RecalledEntry {
|
||||
|
@ -92,6 +93,9 @@ export class KnowledgeBaseService {
|
|||
}
|
||||
|
||||
setup = async () => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return;
|
||||
}
|
||||
const elserModelId = await this.dependencies.getModelId();
|
||||
|
||||
const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 };
|
||||
|
@ -113,9 +117,9 @@ export class KnowledgeBaseService {
|
|||
} catch (error) {
|
||||
if (isModelMissingOrUnavailableError(error)) {
|
||||
return false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -202,6 +206,9 @@ export class KnowledgeBaseService {
|
|||
};
|
||||
|
||||
private ensureTaskScheduled() {
|
||||
if (!this.dependencies.enabled) {
|
||||
return;
|
||||
}
|
||||
this.dependencies.taskManagerStart
|
||||
.ensureScheduled({
|
||||
taskType: INDEX_QUEUED_DOCUMENTS_TASK_TYPE,
|
||||
|
@ -251,7 +258,7 @@ export class KnowledgeBaseService {
|
|||
}
|
||||
|
||||
async processQueue() {
|
||||
if (!this._queue.length) {
|
||||
if (!this._queue.length || !this.dependencies.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -305,6 +312,9 @@ export class KnowledgeBaseService {
|
|||
}
|
||||
|
||||
status = async () => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return { ready: false, enabled: false };
|
||||
}
|
||||
const elserModelId = await this.dependencies.getModelId();
|
||||
|
||||
try {
|
||||
|
@ -320,11 +330,13 @@ export class KnowledgeBaseService {
|
|||
deployment_state: deploymentState,
|
||||
allocation_state: allocationState,
|
||||
model_name: elserModelId,
|
||||
enabled: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof errors.ResponseError ? error.body.error : String(error),
|
||||
ready: false,
|
||||
enabled: true,
|
||||
model_name: elserModelId,
|
||||
};
|
||||
}
|
||||
|
@ -402,6 +414,9 @@ export class KnowledgeBaseService {
|
|||
}): Promise<{
|
||||
entries: RecalledEntry[];
|
||||
}> => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return { entries: [] };
|
||||
}
|
||||
this.dependencies.logger.debug(
|
||||
() => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"`
|
||||
);
|
||||
|
@ -474,6 +489,9 @@ export class KnowledgeBaseService {
|
|||
namespace: string,
|
||||
user?: { name: string }
|
||||
): Promise<Array<Instruction & { public?: boolean }>> => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
||||
index: resourceNames.aliases.kb,
|
||||
|
@ -514,6 +532,9 @@ export class KnowledgeBaseService {
|
|||
sortBy?: string;
|
||||
sortDirection?: 'asc' | 'desc';
|
||||
}): Promise<{ entries: KnowledgeBaseEntry[] }> => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return { entries: [] };
|
||||
}
|
||||
try {
|
||||
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
||||
index: resourceNames.aliases.kb,
|
||||
|
@ -578,6 +599,9 @@ export class KnowledgeBaseService {
|
|||
user?: { name: string; id?: string };
|
||||
namespace?: string;
|
||||
}) => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return null;
|
||||
}
|
||||
const res = await this.dependencies.esClient.asInternalUser.search<
|
||||
Pick<KnowledgeBaseEntry, 'doc_id'>
|
||||
>({
|
||||
|
@ -607,6 +631,9 @@ export class KnowledgeBaseService {
|
|||
user?: { name: string; id?: string };
|
||||
namespace?: string;
|
||||
}): Promise<void> => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return;
|
||||
}
|
||||
// for now we want to limit the number of user instructions to 1 per user
|
||||
if (document.type === KnowledgeBaseType.UserInstruction) {
|
||||
const existingId = await this.getExistingUserInstructionId({
|
||||
|
@ -647,6 +674,9 @@ export class KnowledgeBaseService {
|
|||
}: {
|
||||
operations: KnowledgeBaseEntryOperation[];
|
||||
}): Promise<void> => {
|
||||
if (!this.dependencies.enabled) {
|
||||
return;
|
||||
}
|
||||
this.dependencies.logger.info(`Starting import of ${operations.length} entries`);
|
||||
|
||||
const limiter = pLimit(5);
|
||||
|
|
|
@ -164,7 +164,7 @@ export function NavControl() {
|
|||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
navigateToConversation={(conversationId: string) => {
|
||||
navigateToConversation={(conversationId?: string) => {
|
||||
application.navigateToUrl(
|
||||
http.basePath.prepend(
|
||||
`/app/observabilityAIAssistant/conversations/${conversationId || ''}`
|
||||
|
|
|
@ -6,9 +6,24 @@
|
|||
"id": "observabilityAiAssistantManagement",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "observabilityAiAssistantManagement"],
|
||||
"requiredPlugins": ["management", "observabilityAIAssistant", "observabilityShared"],
|
||||
"optionalPlugins": ["actions", "home", "serverless", "enterpriseSearch"],
|
||||
"requiredBundles": ["kibanaReact", "logsDataAccess"]
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"observabilityAiAssistantManagement"
|
||||
],
|
||||
"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 { wrapWithTheme } from '@kbn/kibana-react-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 { RedirectToHomeIfUnauthorized } from './routes/components/redirect_to_home_if_unauthorized';
|
||||
import { AppContextProvider } from './context/app_context';
|
||||
|
@ -23,9 +27,10 @@ import { AppContextProvider } from './context/app_context';
|
|||
interface MountParams {
|
||||
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>;
|
||||
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();
|
||||
|
||||
if (!startDeps.observabilityAIAssistant) return () => {};
|
||||
|
@ -46,7 +51,7 @@ export const mountManagementSection = async ({ core, mountParams }: MountParams)
|
|||
<RedirectToHomeIfUnauthorized coreStart={coreStart}>
|
||||
<I18nProvider>
|
||||
<KibanaContextProvider services={{ ...coreStart, ...startDeps }}>
|
||||
<AppContextProvider value={{ setBreadcrumbs }}>
|
||||
<AppContextProvider value={{ setBreadcrumbs, config }}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider
|
||||
history={history}
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import React, { createContext } from 'react';
|
||||
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
|
||||
import { ConfigSchema } from '../plugin';
|
||||
|
||||
export interface AppContextValue {
|
||||
setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void;
|
||||
config: ConfigSchema;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContextValue>(null as any);
|
||||
|
|
|
@ -70,6 +70,11 @@ export const render = (
|
|||
|
||||
const appContextValue = mocks?.appContextValue ?? {
|
||||
setBreadcrumbs: () => {},
|
||||
config: {
|
||||
logSourcesEnabled: true,
|
||||
spacesEnabled: true,
|
||||
visibilityEnabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
return testLibRender(
|
||||
|
|
|
@ -5,13 +5,26 @@
|
|||
* 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 {
|
||||
AiAssistantManagementObservabilityPluginSetup,
|
||||
AiAssistantManagementObservabilityPluginStart,
|
||||
} from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new AiAssistantManagementObservabilityPlugin();
|
||||
}
|
||||
export const plugin: PluginInitializer<
|
||||
AiAssistantManagementObservabilityPluginSetup,
|
||||
AiAssistantManagementObservabilityPluginStart,
|
||||
SetupDependencies,
|
||||
StartDependencies
|
||||
> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) => {
|
||||
return new AiAssistantManagementObservabilityPlugin(pluginInitializerContext);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
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 { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
||||
import { ServerlessPluginStart } from '@kbn/serverless/public';
|
||||
|
@ -35,6 +35,12 @@ export interface StartDependencies {
|
|||
enterpriseSearch?: EnterpriseSearchPublicStart;
|
||||
}
|
||||
|
||||
export interface ConfigSchema {
|
||||
logSourcesEnabled: boolean;
|
||||
spacesEnabled: boolean;
|
||||
visibilityEnabled: boolean;
|
||||
}
|
||||
|
||||
export class AiAssistantManagementObservabilityPlugin
|
||||
implements
|
||||
Plugin<
|
||||
|
@ -44,6 +50,12 @@ export class AiAssistantManagementObservabilityPlugin
|
|||
StartDependencies
|
||||
>
|
||||
{
|
||||
private readonly config: ConfigSchema;
|
||||
|
||||
constructor(context: PluginInitializerContext<ConfigSchema>) {
|
||||
this.config = context.config.get();
|
||||
}
|
||||
|
||||
public setup(
|
||||
core: CoreSetup<StartDependencies, AiAssistantManagementObservabilityPluginStart>,
|
||||
{ home, management, observabilityAIAssistant }: SetupDependencies
|
||||
|
@ -78,6 +90,7 @@ export class AiAssistantManagementObservabilityPlugin
|
|||
return mountManagementSection({
|
||||
core,
|
||||
mountParams,
|
||||
config: this.config,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,8 +8,24 @@
|
|||
import React from 'react';
|
||||
import { coreStartMock, render } from '../../helpers/test_helper';
|
||||
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', () => {
|
||||
const appContextValue = {
|
||||
config: { spacesEnabled: true, visibilityEnabled: true, logSourcesEnabled: true },
|
||||
setBreadcrumbs: () => {},
|
||||
};
|
||||
useKnowledgeBaseMock.mockReturnValue({
|
||||
status: {
|
||||
value: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
it('should navigate to home when not authorized', () => {
|
||||
render(<SettingsPage />, {
|
||||
coreStart: {
|
||||
|
@ -21,13 +37,16 @@ describe('Settings Page', () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
appContextValue,
|
||||
});
|
||||
|
||||
expect(coreStartMock.application.navigateToApp).toBeCalledWith('home');
|
||||
});
|
||||
|
||||
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-knowledge_base')).toBeInTheDocument();
|
||||
|
@ -36,7 +55,7 @@ describe('Settings Page', () => {
|
|||
it('should set breadcrumbs', () => {
|
||||
const setBreadcrumbs = jest.fn();
|
||||
render(<SettingsPage />, {
|
||||
appContextValue: { setBreadcrumbs },
|
||||
appContextValue: { ...appContextValue, setBreadcrumbs },
|
||||
});
|
||||
|
||||
expect(setBreadcrumbs).toHaveBeenCalledWith([
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui';
|
||||
import { useKnowledgeBase } from '@kbn/ai-assistant';
|
||||
import { useAppContext } from '../../hooks/use_app_context';
|
||||
import { SettingsTab } from './settings_tab/settings_tab';
|
||||
import { KnowledgeBaseTab } from './knowledge_base_tab';
|
||||
|
@ -28,6 +29,7 @@ export function SettingsPage() {
|
|||
} = useKibana();
|
||||
|
||||
const router = useObservabilityAIAssistantManagementRouter();
|
||||
const knowledgeBase = useKnowledgeBase();
|
||||
|
||||
const {
|
||||
query: { tab },
|
||||
|
@ -85,6 +87,7 @@ export function SettingsPage() {
|
|||
}
|
||||
),
|
||||
content: <KnowledgeBaseTab />,
|
||||
disabled: !knowledgeBase.status.value?.enabled,
|
||||
},
|
||||
{
|
||||
id: 'search_connector',
|
||||
|
|
|
@ -9,10 +9,14 @@ import React from 'react';
|
|||
import { fireEvent } from '@testing-library/react';
|
||||
import { render } from '../../../helpers/test_helper';
|
||||
import { SettingsTab } from './settings_tab';
|
||||
import { useAppContext } from '../../../hooks/use_app_context';
|
||||
|
||||
jest.mock('../../../hooks/use_app_context');
|
||||
|
||||
const useAppContextMock = useAppContext as jest.Mock;
|
||||
|
||||
describe('SettingsTab', () => {
|
||||
useAppContextMock.mockReturnValue({ config: { spacesEnabled: true, visibilityEnabled: true } });
|
||||
it('should offer a way to configure Observability AI Assistant visibility in apps', () => {
|
||||
const navigateToAppMock = jest.fn(() => Promise.resolve());
|
||||
const { getByTestId } = render(<SettingsTab />, {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React from 'react';
|
||||
import { EuiButton, EuiDescribedFormGroup, EuiFormRow, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAppContext } from '../../../hooks/use_app_context';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { UISettings } from './ui_settings';
|
||||
|
||||
|
@ -15,6 +16,7 @@ export function SettingsTab() {
|
|||
const {
|
||||
application: { navigateToApp },
|
||||
} = useKibana().services;
|
||||
const { config } = useAppContext();
|
||||
|
||||
const handleNavigateToConnectors = () => {
|
||||
navigateToApp('management', {
|
||||
|
@ -30,44 +32,46 @@ export function SettingsTab() {
|
|||
|
||||
return (
|
||||
<EuiPanel hasBorder grow={false}>
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Show AI Assistant button and Contextual Insights in Observability apps',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
}
|
||||
description={
|
||||
<p>
|
||||
{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.',
|
||||
ignoreTag: true,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiButton
|
||||
data-test-subj="settingsTabGoToSpacesButton"
|
||||
onClick={handleNavigateToSpacesConfiguration}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
|
||||
{ defaultMessage: 'Go to Spaces' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
{config.spacesEnabled && (
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.settingsPage.showAIAssistantButtonLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Show AI Assistant button and Contextual Insights in Observability apps',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
}
|
||||
description={
|
||||
<p>
|
||||
{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.',
|
||||
ignoreTag: true,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiButton
|
||||
data-test-subj="settingsTabGoToSpacesButton"
|
||||
onClick={handleNavigateToSpacesConfiguration}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.settingsPage.goToFeatureControlsButtonLabel',
|
||||
{ defaultMessage: 'Go to Spaces' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
)}
|
||||
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
|
|
|
@ -18,14 +18,10 @@ import { EuiSpacer } from '@elastic/eui';
|
|||
import { isEmpty } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
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';
|
||||
|
||||
const settingsKeys = [
|
||||
aiAssistantSimulatedFunctionCalling,
|
||||
aiAssistantSearchConnectorIndexPattern,
|
||||
aiAssistantPreferredAIAssistantType,
|
||||
];
|
||||
|
||||
export function UISettings() {
|
||||
const {
|
||||
docLinks,
|
||||
|
@ -33,6 +29,14 @@ export function UISettings() {
|
|||
notifications,
|
||||
application: { capabilities, getUrlForApp },
|
||||
} = 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 } =
|
||||
useEditableSettings(settingsKeys);
|
||||
|
@ -84,12 +88,13 @@ export function UISettings() {
|
|||
</FieldRowProvider>
|
||||
);
|
||||
})}
|
||||
|
||||
<LogSourcesSettingSynchronisationInfo
|
||||
isLoading={false}
|
||||
logSourcesValue={settings.client.get(aiAssistantLogsIndexPattern)}
|
||||
getUrlForApp={getUrlForApp}
|
||||
/>
|
||||
{config.logSourcesEnabled && (
|
||||
<LogSourcesSettingSynchronisationInfo
|
||||
isLoading={false}
|
||||
logSourcesValue={settings.client.get(aiAssistantLogsIndexPattern)}
|
||||
getUrlForApp={getUrlForApp}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isEmpty(unsavedChanges) && (
|
||||
<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.
|
||||
*/
|
||||
|
||||
export { config } from './config';
|
||||
|
||||
export const plugin = async () => {
|
||||
const { AiAssistantManagementPlugin } = await import('./plugin');
|
||||
return new AiAssistantManagementPlugin();
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*"],
|
||||
"include": [
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/home-plugin",
|
||||
|
@ -22,6 +26,11 @@
|
|||
"@kbn/config-schema",
|
||||
"@kbn/core-ui-settings-common",
|
||||
"@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",
|
||||
"licensing",
|
||||
"observabilityAIAssistant",
|
||||
"observabilityAIAssistantApp",
|
||||
"triggersActionsUi",
|
||||
"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.
|
||||
*/
|
||||
|
||||
import {
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
type CoreSetup,
|
||||
type Plugin,
|
||||
CoreStart,
|
||||
AppMountParameters,
|
||||
PluginInitializerContext,
|
||||
} from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type CoreSetup, type Plugin, CoreStart, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { createAppService } from '@kbn/ai-assistant';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
import type {
|
||||
SearchAssistantPluginSetup,
|
||||
SearchAssistantPluginStart,
|
||||
SearchAssistantPluginStartDependencies,
|
||||
} from './types';
|
||||
import { NavControlInitiator } from './components/nav_control/lazy_nav_control';
|
||||
|
||||
export interface PublicConfigType {
|
||||
ui: {
|
||||
|
@ -44,36 +40,43 @@ export class SearchAssistantPlugin
|
|||
public setup(
|
||||
core: CoreSetup<SearchAssistantPluginStartDependencies, SearchAssistantPluginStart>
|
||||
): 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 {};
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
import { UsageCollectionStart } from '@kbn/usage-collection-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
|
||||
export interface SearchAssistantPluginSetup {}
|
||||
|
@ -15,6 +19,10 @@ export interface SearchAssistantPluginSetup {}
|
|||
export interface SearchAssistantPluginStart {}
|
||||
|
||||
export interface SearchAssistantPluginStartDependencies {
|
||||
licensing: LicensingPluginStart;
|
||||
ml: MlPluginStart;
|
||||
observabilityAIAssistant: ObservabilityAIAssistantPublicStart;
|
||||
share: SharePluginStart;
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
|
|
@ -13,17 +13,22 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/shared-ux-page-kibana-template",
|
||||
"@kbn/usage-collection-plugin",
|
||||
"@kbn/observability-ai-assistant-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/ai-assistant",
|
||||
"@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": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -13,12 +13,12 @@
|
|||
"requiredPlugins": [
|
||||
"actions",
|
||||
"features",
|
||||
"ml",
|
||||
"share",
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"cloud",
|
||||
"console",
|
||||
"ml"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"kibanaReact"
|
||||
|
|
|
@ -49,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
expect(res.body).to.eql({
|
||||
ready: false,
|
||||
model_name: TINY_ELSER.id,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -69,6 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
expect(res.body).to.eql({
|
||||
ready: false,
|
||||
model_name: TINY_ELSER.id,
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,5 +64,8 @@ export function SvlSearchHomePageProvider({ getService }: FtrProviderContext) {
|
|||
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.closeConnectionDetailsFlyout();
|
||||
});
|
||||
|
||||
it('shows the AI assistant', async () => {
|
||||
await pageObjects.svlSearchHomePage.expectAIAssistantToExist();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue