[Security solution] assistantKnowledgeBaseByDefault flag removed (#198180)

This commit is contained in:
Steph Milovic 2024-11-12 11:10:53 -07:00 committed by GitHub
parent 0ab841f6d6
commit 194de0dadb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 254 additions and 3169 deletions

View file

@ -997,7 +997,7 @@
"\nInterface for features available to the elastic assistant"
],
"signature": [
"{ readonly assistantKnowledgeBaseByDefault: boolean; readonly assistantModelEvaluation: boolean; }"
"{ readonly assistantModelEvaluation: boolean; }"
],
"path": "x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts",
"deprecated": false,
@ -2772,7 +2772,7 @@
"label": "GetCapabilitiesResponse",
"description": [],
"signature": [
"{ assistantKnowledgeBaseByDefault: boolean; assistantModelEvaluation: boolean; }"
"{ assistantModelEvaluation: boolean; }"
],
"path": "x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts",
"deprecated": false,
@ -4767,7 +4767,7 @@
"\nDefault features available to the elastic assistant"
],
"signature": [
"{ readonly assistantKnowledgeBaseByDefault: true; readonly assistantModelEvaluation: false; }"
"{ readonly assistantModelEvaluation: false; }"
],
"path": "x-pack/packages/kbn-elastic-assistant-common/impl/capabilities/index.ts",
"deprecated": false,
@ -5232,7 +5232,7 @@
"label": "GetCapabilitiesResponse",
"description": [],
"signature": [
"Zod.ZodObject<{ assistantKnowledgeBaseByDefault: Zod.ZodBoolean; assistantModelEvaluation: Zod.ZodBoolean; }, \"strip\", Zod.ZodTypeAny, { assistantKnowledgeBaseByDefault: boolean; assistantModelEvaluation: boolean; }, { assistantKnowledgeBaseByDefault: boolean; assistantModelEvaluation: boolean; }>"
"Zod.ZodObject<{ assistantModelEvaluation: Zod.ZodBoolean; }, \"strip\", Zod.ZodTypeAny, { assistantModelEvaluation: boolean; }, { assistantModelEvaluation: boolean; }>"
],
"path": "x-pack/packages/kbn-elastic-assistant-common/impl/schemas/capabilities/get_capabilities_route.gen.ts",
"deprecated": false,
@ -6201,4 +6201,4 @@
}
]
}
}
}

View file

@ -420,7 +420,7 @@
"\nExperimental flag needed to enable the link"
],
"signature": [
"\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined"
"\"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined"
],
"path": "x-pack/plugins/security_solution/public/common/links/types.ts",
"deprecated": false,
@ -500,7 +500,7 @@
"\nExperimental flag needed to disable the link. Opposite of experimentalKey"
],
"signature": [
"\"assistantKnowledgeBaseByDefault\" | \"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined"
"\"assistantModelEvaluation\" | \"excludePoliciesInFilterEnabled\" | \"kubernetesEnabled\" | \"donutChartEmbeddablesEnabled\" | \"previewTelemetryUrlEnabled\" | \"extendedRuleExecutionLoggingEnabled\" | \"socTrendsEnabled\" | \"responseActionUploadEnabled\" | \"automatedProcessActionsEnabled\" | \"responseActionsSentinelOneV1Enabled\" | \"responseActionsSentinelOneV2Enabled\" | \"responseActionsSentinelOneGetFileEnabled\" | \"responseActionsSentinelOneKillProcessEnabled\" | \"responseActionsSentinelOneProcessesEnabled\" | \"responseActionsCrowdstrikeManualHostIsolationEnabled\" | \"endpointManagementSpaceAwarenessEnabled\" | \"securitySolutionNotesDisabled\" | \"entityAlertPreviewDisabled\" | \"newUserDetailsFlyoutManagedUser\" | \"riskScoringPersistence\" | \"riskScoringRoutesEnabled\" | \"esqlRulesDisabled\" | \"protectionUpdatesEnabled\" | \"disableTimelineSaveTour\" | \"riskEnginePrivilegesRouteEnabled\" | \"sentinelOneDataInAnalyzerEnabled\" | \"sentinelOneManualHostActionsEnabled\" | \"crowdstrikeDataInAnalyzerEnabled\" | \"responseActionsTelemetryEnabled\" | \"jamfDataInAnalyzerEnabled\" | \"timelineEsqlTabDisabled\" | \"analyzerDatePickersAndSourcererDisabled\" | \"graphVisualizationInFlyoutEnabled\" | \"prebuiltRulesCustomizationEnabled\" | \"malwareOnWriteScanOptionAvailable\" | \"unifiedManifestEnabled\" | \"valueListItemsModalEnabled\" | \"filterProcessDescendantsForEventFiltersEnabled\" | \"dataIngestionHubEnabled\" | \"entityStoreDisabled\" | \"siemMigrationsEnabled\" | undefined"
],
"path": "x-pack/plugins/security_solution/public/common/links/types.ts",
"deprecated": false,
@ -1791,7 +1791,7 @@
"label": "experimentalFeatures",
"description": [],
"signature": [
"{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }"
"{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }"
],
"path": "x-pack/plugins/security_solution/public/types.ts",
"deprecated": false,
@ -3039,7 +3039,7 @@
"\nThe security solution generic experimental features"
],
"signature": [
"{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }"
"{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }"
],
"path": "x-pack/plugins/security_solution/server/plugin_contract.ts",
"deprecated": false,
@ -3212,7 +3212,7 @@
"label": "ExperimentalFeatures",
"description": [],
"signature": [
"{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly assistantKnowledgeBaseByDefault: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }"
"{ readonly excludePoliciesInFilterEnabled: boolean; readonly kubernetesEnabled: boolean; readonly donutChartEmbeddablesEnabled: boolean; readonly previewTelemetryUrlEnabled: boolean; readonly extendedRuleExecutionLoggingEnabled: boolean; readonly socTrendsEnabled: boolean; readonly responseActionUploadEnabled: boolean; readonly automatedProcessActionsEnabled: boolean; readonly responseActionsSentinelOneV1Enabled: boolean; readonly responseActionsSentinelOneV2Enabled: boolean; readonly responseActionsSentinelOneGetFileEnabled: boolean; readonly responseActionsSentinelOneKillProcessEnabled: boolean; readonly responseActionsSentinelOneProcessesEnabled: boolean; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: boolean; readonly endpointManagementSpaceAwarenessEnabled: boolean; readonly securitySolutionNotesDisabled: boolean; readonly entityAlertPreviewDisabled: boolean; readonly assistantModelEvaluation: boolean; readonly newUserDetailsFlyoutManagedUser: boolean; readonly riskScoringPersistence: boolean; readonly riskScoringRoutesEnabled: boolean; readonly esqlRulesDisabled: boolean; readonly protectionUpdatesEnabled: boolean; readonly disableTimelineSaveTour: boolean; readonly riskEnginePrivilegesRouteEnabled: boolean; readonly sentinelOneDataInAnalyzerEnabled: boolean; readonly sentinelOneManualHostActionsEnabled: boolean; readonly crowdstrikeDataInAnalyzerEnabled: boolean; readonly responseActionsTelemetryEnabled: boolean; readonly jamfDataInAnalyzerEnabled: boolean; readonly timelineEsqlTabDisabled: boolean; readonly analyzerDatePickersAndSourcererDisabled: boolean; readonly graphVisualizationInFlyoutEnabled: boolean; readonly prebuiltRulesCustomizationEnabled: boolean; readonly malwareOnWriteScanOptionAvailable: boolean; readonly unifiedManifestEnabled: boolean; readonly valueListItemsModalEnabled: boolean; readonly filterProcessDescendantsForEventFiltersEnabled: boolean; readonly dataIngestionHubEnabled: boolean; readonly entityStoreDisabled: boolean; readonly siemMigrationsEnabled: boolean; }"
],
"path": "x-pack/plugins/security_solution/common/experimental_features.ts",
"deprecated": false,
@ -3278,7 +3278,7 @@
"\nA list of allowed values that can be used in `xpack.securitySolution.enableExperimental`.\nThis object is then used to validate and parse the value entered."
],
"signature": [
"{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesDisabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly assistantKnowledgeBaseByDefault: true; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly graphVisualizationInFlyoutEnabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreDisabled: false; readonly siemMigrationsEnabled: false; }"
"{ readonly excludePoliciesInFilterEnabled: false; readonly kubernetesEnabled: true; readonly donutChartEmbeddablesEnabled: false; readonly previewTelemetryUrlEnabled: false; readonly extendedRuleExecutionLoggingEnabled: false; readonly socTrendsEnabled: false; readonly responseActionUploadEnabled: true; readonly automatedProcessActionsEnabled: true; readonly responseActionsSentinelOneV1Enabled: true; readonly responseActionsSentinelOneV2Enabled: true; readonly responseActionsSentinelOneGetFileEnabled: true; readonly responseActionsSentinelOneKillProcessEnabled: true; readonly responseActionsSentinelOneProcessesEnabled: true; readonly responseActionsCrowdstrikeManualHostIsolationEnabled: true; readonly endpointManagementSpaceAwarenessEnabled: false; readonly securitySolutionNotesDisabled: false; readonly entityAlertPreviewDisabled: false; readonly assistantModelEvaluation: false; readonly newUserDetailsFlyoutManagedUser: false; readonly riskScoringPersistence: true; readonly riskScoringRoutesEnabled: true; readonly esqlRulesDisabled: false; readonly protectionUpdatesEnabled: true; readonly disableTimelineSaveTour: false; readonly riskEnginePrivilegesRouteEnabled: true; readonly sentinelOneDataInAnalyzerEnabled: true; readonly sentinelOneManualHostActionsEnabled: true; readonly crowdstrikeDataInAnalyzerEnabled: true; readonly responseActionsTelemetryEnabled: false; readonly jamfDataInAnalyzerEnabled: true; readonly timelineEsqlTabDisabled: false; readonly analyzerDatePickersAndSourcererDisabled: false; readonly graphVisualizationInFlyoutEnabled: false; readonly prebuiltRulesCustomizationEnabled: false; readonly malwareOnWriteScanOptionAvailable: true; readonly unifiedManifestEnabled: true; readonly valueListItemsModalEnabled: true; readonly filterProcessDescendantsForEventFiltersEnabled: true; readonly dataIngestionHubEnabled: false; readonly entityStoreDisabled: false; readonly siemMigrationsEnabled: false; }"
],
"path": "x-pack/plugins/security_solution/common/experimental_features.ts",
"deprecated": false,
@ -3287,4 +3287,4 @@
}
]
}
}
}

View file

@ -19,6 +19,5 @@ export type AssistantFeatureKey = keyof AssistantFeatures;
* Default features available to the elastic assistant
*/
export const defaultAssistantFeatures = Object.freeze({
assistantKnowledgeBaseByDefault: true,
assistantModelEvaluation: false,
});

View file

@ -18,6 +18,5 @@ import { z } from '@kbn/zod';
export type GetCapabilitiesResponse = z.infer<typeof GetCapabilitiesResponse>;
export const GetCapabilitiesResponse = z.object({
assistantKnowledgeBaseByDefault: z.boolean(),
assistantModelEvaluation: z.boolean(),
});

View file

@ -20,12 +20,9 @@ paths:
schema:
type: object
properties:
assistantKnowledgeBaseByDefault:
type: boolean
assistantModelEvaluation:
type: boolean
required:
- assistantKnowledgeBaseByDefault
- assistantModelEvaluation
'400':
description: Generic Error

View file

@ -55,20 +55,6 @@ export type CreateKnowledgeBaseRequestParamsInput = z.input<
export type CreateKnowledgeBaseResponse = z.infer<typeof CreateKnowledgeBaseResponse>;
export const CreateKnowledgeBaseResponse = KnowledgeBaseResponse;
export type DeleteKnowledgeBaseRequestParams = z.infer<typeof DeleteKnowledgeBaseRequestParams>;
export const DeleteKnowledgeBaseRequestParams = z.object({
/**
* The KnowledgeBase `resource` value.
*/
resource: z.string().optional(),
});
export type DeleteKnowledgeBaseRequestParamsInput = z.input<
typeof DeleteKnowledgeBaseRequestParams
>;
export type DeleteKnowledgeBaseResponse = z.infer<typeof DeleteKnowledgeBaseResponse>;
export const DeleteKnowledgeBaseResponse = KnowledgeBaseResponse;
export type ReadKnowledgeBaseRequestParams = z.infer<typeof ReadKnowledgeBaseRequestParams>;
export const ReadKnowledgeBaseRequestParams = z.object({
/**

View file

@ -100,40 +100,6 @@ paths:
type: string
message:
type: string
delete:
x-codegen-enabled: true
x-labels: [ess, serverless]
operationId: DeleteKnowledgeBase
description: Deletes KnowledgeBase with the `resource` field.
summary: Deletes a KnowledgeBase
tags:
- KnowledgeBase API
parameters:
- name: resource
in: path
description: The KnowledgeBase `resource` value.
schema:
type: string
responses:
200:
description: Indicates a successful call.
content:
application/json:
schema:
$ref: '#/components/schemas/KnowledgeBaseResponse'
400:
description: Generic Error
content:
application/json:
schema:
type: object
properties:
statusCode:
type: number
error:
type: string
message:
type: string
components:
schemas:

View file

@ -6,7 +6,7 @@ paths:
/internal/elastic_assistant/knowledge_base/entries/_bulk_action:
post:
x-codegen-enabled: true
# This API is still behind the `assistantKnowledgeBaseByDefault` feature flag
# Targeted to update to public by 8.18
x-internal: true
x-labels: [ess, serverless]
operationId: PerformKnowledgeBaseEntryBulkAction

View file

@ -6,7 +6,7 @@ paths:
/internal/elastic_assistant/knowledge_base/entries:
post:
x-codegen-enabled: true
# This API is still behind the `assistantKnowledgeBaseByDefault` feature flag
# Targeted to update to public by 8.18
x-internal: true
x-labels: [ess, serverless]
operationId: CreateKnowledgeBaseEntry
@ -37,7 +37,7 @@ paths:
/internal/elastic_assistant/knowledge_base/entries/{id}:
get:
x-codegen-enabled: true
# This API is still behind the `assistantKnowledgeBaseByDefault` feature flag
# Targeted to update to public by 8.18
x-internal: true
x-labels: [ess, serverless]
operationId: ReadKnowledgeBaseEntry
@ -67,7 +67,7 @@ paths:
$ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema'
put:
x-codegen-enabled: true
# This API is still behind the `assistantKnowledgeBaseByDefault` feature flag
# Targeted to update to public by 8.18
x-internal: true
x-labels: [ess, serverless]
operationId: UpdateKnowledgeBaseEntry
@ -103,7 +103,7 @@ paths:
$ref: './common_attributes.schema.yaml#/components/schemas/KnowledgeBaseEntryErrorSchema'
delete:
x-codegen-enabled: true
# This API is still behind the `assistantKnowledgeBaseByDefault` feature flag
# Targeted to update to public by 8.18
x-internal: true
x-labels: [ess, serverless]
operationId: DeleteKnowledgeBaseEntry

View file

@ -6,7 +6,7 @@ paths:
/internal/elastic_assistant/knowledge_base/entries/_find:
get:
x-codegen-enabled: true
# This API is still behind the `assistantKnowledgeBaseByDefault` feature flag
# Targeted to update to public by 8.18
x-internal: true
x-labels: [ess, serverless]
operationId: FindKnowledgeBaseEntries

View file

@ -7,12 +7,7 @@
import { HttpSetup } from '@kbn/core-http-browser';
import {
deleteKnowledgeBase,
getKnowledgeBaseIndices,
getKnowledgeBaseStatus,
postKnowledgeBase,
} from './api';
import { getKnowledgeBaseIndices, getKnowledgeBaseStatus, postKnowledgeBase } from './api';
jest.mock('@kbn/core-http-browser');
@ -78,29 +73,6 @@ describe('API tests', () => {
});
});
describe('deleteKnowledgeBase', () => {
it('calls the knowledge base API when correct resource path', async () => {
await deleteKnowledgeBase(knowledgeBaseArgs);
expect(mockHttp.fetch).toHaveBeenCalledWith(
'/internal/elastic_assistant/knowledge_base/a-resource',
{
method: 'DELETE',
signal: undefined,
version: '1',
}
);
});
it('returns error when error is an error', async () => {
const error = 'simulated error';
(mockHttp.fetch as jest.Mock).mockImplementation(() => {
throw new Error(error);
});
await expect(deleteKnowledgeBase(knowledgeBaseArgs)).resolves.toThrowError('simulated error');
});
});
describe('getKnowledgeBaseIndices', () => {
it('calls the knowledge base API when correct resource path', async () => {
await getKnowledgeBaseIndices({ http: mockHttp });

View file

@ -9,8 +9,6 @@ import {
API_VERSIONS,
CreateKnowledgeBaseRequestParams,
CreateKnowledgeBaseResponse,
DeleteKnowledgeBaseRequestParams,
DeleteKnowledgeBaseResponse,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_INDICES_URL,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL,
GetKnowledgeBaseIndicesResponse,
@ -79,38 +77,6 @@ export const postKnowledgeBase = async ({
return response as CreateKnowledgeBaseResponse;
};
/**
* API call for deleting the Knowledge Base. Provide a resource to delete that specific resource.
*
* @param {Object} options - The options object.
* @param {HttpSetup} options.http - HttpSetup
* @param {string} [options.resource] - Resource to be deleted from the KB, otherwise delete the entire KB
* @param {AbortSignal} [options.signal] - AbortSignal
*
* @returns {Promise<DeleteKnowledgeBaseResponse | IHttpFetchError>}
*/
export const deleteKnowledgeBase = async ({
http,
resource,
signal,
}: DeleteKnowledgeBaseRequestParams & {
http: HttpSetup;
signal?: AbortSignal | undefined;
}): Promise<DeleteKnowledgeBaseResponse | IHttpFetchError> => {
try {
const path = ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL.replace('{resource?}', resource || '');
const response = await http.fetch(path, {
method: 'DELETE',
signal,
version: API_VERSIONS.internal.v1,
});
return response as DeleteKnowledgeBaseResponse;
} catch (error) {
return error as IHttpFetchError;
}
};
/**
* API call for getting indices that have fields of `semantic_text` type.
*

View file

@ -1,110 +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 { act, renderHook } from '@testing-library/react-hooks';
import { useDeleteKnowledgeBase, UseDeleteKnowledgeBaseParams } from './use_delete_knowledge_base';
import { deleteKnowledgeBase as _deleteKnowledgeBase } from './api';
import { useMutation as _useMutation } from '@tanstack/react-query';
const useMutationMock = _useMutation as jest.Mock;
const deleteKnowledgeBaseMock = _deleteKnowledgeBase as jest.Mock;
jest.mock('./api', () => {
const actual = jest.requireActual('./api');
return {
...actual,
deleteKnowledgeBase: jest.fn((...args) => actual.deleteKnowledgeBase(...args)),
};
});
jest.mock('./use_knowledge_base_status');
jest.mock('@tanstack/react-query', () => ({
useMutation: jest.fn().mockImplementation(async (queryKey, fn, opts) => {
try {
const res = await fn();
return Promise.resolve(res);
} catch (e) {
opts.onError(e);
}
}),
}));
const statusResponse = {
success: true,
};
const http = {
fetch: jest.fn().mockResolvedValue(statusResponse),
};
const toasts = {
addError: jest.fn(),
};
const defaultProps = { http, toasts } as unknown as UseDeleteKnowledgeBaseParams;
describe('useDeleteKnowledgeBase', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should call api to delete knowledge base', async () => {
await act(async () => {
const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
await waitForNextUpdate();
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
'/internal/elastic_assistant/knowledge_base/',
{
method: 'DELETE',
signal: undefined,
version: '1',
}
);
expect(toasts.addError).not.toHaveBeenCalled();
});
});
it('should call api to delete knowledge base with resource arg', async () => {
useMutationMock.mockImplementation(async (queryKey, fn, opts) => {
try {
const res = await fn('something');
return Promise.resolve(res);
} catch (e) {
opts.onError(e);
}
});
await act(async () => {
const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
await waitForNextUpdate();
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
'/internal/elastic_assistant/knowledge_base/something',
{
method: 'DELETE',
signal: undefined,
version: '1',
}
);
});
});
it('should return delete response', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
await waitForNextUpdate();
await expect(result.current).resolves.toStrictEqual(statusResponse);
});
});
it('should display error toast when api throws error', async () => {
deleteKnowledgeBaseMock.mockRejectedValue(new Error('this is an error'));
await act(async () => {
const { waitForNextUpdate } = renderHook(() => useDeleteKnowledgeBase(defaultProps));
await waitForNextUpdate();
expect(toasts.addError).toHaveBeenCalled();
});
});
});

View file

@ -1,58 +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 { useMutation } from '@tanstack/react-query';
import type { IToasts } from '@kbn/core-notifications-browser';
import type { HttpSetup, IHttpFetchError, ResponseErrorBody } from '@kbn/core-http-browser';
import { i18n } from '@kbn/i18n';
import { deleteKnowledgeBase } from './api';
import { useInvalidateKnowledgeBaseStatus } from './use_knowledge_base_status';
const DELETE_KNOWLEDGE_BASE_MUTATION_KEY = ['elastic-assistant', 'delete-knowledge-base'];
export interface UseDeleteKnowledgeBaseParams {
http: HttpSetup;
toasts?: IToasts;
}
/**
* Hook for deleting the Knowledge Base. Provide a resource name to delete a
* specific resource within KB.
*
* @param {Object} options - The options object.
* @param {HttpSetup} options.http - HttpSetup
* @param {IToasts} [options.toasts] - IToasts
*
* @returns {useMutation} hook for deleting the Knowledge Base
*/
export const useDeleteKnowledgeBase = ({ http, toasts }: UseDeleteKnowledgeBaseParams) => {
const invalidateKnowledgeBaseStatus = useInvalidateKnowledgeBaseStatus();
return useMutation(
DELETE_KNOWLEDGE_BASE_MUTATION_KEY,
(resource?: string | void) => {
// Optional params workaround: see: https://github.com/TanStack/query/issues/1077#issuecomment-1431247266
return deleteKnowledgeBase({ http, resource: resource ?? undefined });
},
{
onError: (error: IHttpFetchError<ResponseErrorBody>) => {
if (error.name !== 'AbortError') {
toasts?.addError(
error.body && error.body.message ? new Error(error.body.message) : error,
{
title: i18n.translate('xpack.elasticAssistant.knowledgeBase.deleteError', {
defaultMessage: 'Error deleting Knowledge Base',
}),
}
);
}
},
onSettled: () => {
invalidateKnowledgeBaseStatus();
},
}
);
};

View file

@ -23,7 +23,7 @@ import { Conversation } from '../../..';
import { AssistantTitle } from '../assistant_title';
import { ConnectorSelectorInline } from '../../connectorland/connector_selector_inline/connector_selector_inline';
import { FlyoutNavigation } from '../assistant_overlay/flyout_navigation';
import { AssistantSettingsButton } from '../settings/assistant_settings_button';
import { AssistantSettingsModal } from '../settings/assistant_settings_modal';
import * as i18n from './translations';
import { AIConnector } from '../../connectorland/connector_selector';
import { SettingsContextMenu } from '../settings/settings_context_menu/settings_context_menu';
@ -113,7 +113,7 @@ export const AssistantHeader: React.FC<Props> = ({
>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AssistantSettingsButton
<AssistantSettingsModal
defaultConnector={defaultConnector}
isDisabled={isDisabled}
isSettingsModalVisible={isSettingsModalVisible}

View file

@ -38,7 +38,6 @@ const mockContext = {
basePromptContexts: MOCK_QUICK_PROMPTS,
setSelectedSettingsTab,
http: {},
assistantFeatures: { assistantModelEvaluation: true, assistantKnowledgeBaseByDefault: false },
selectedSettingsTab: 'CONVERSATIONS_TAB',
assistantAvailability: {
isAssistantEnabled: true,
@ -136,17 +135,6 @@ describe('AssistantSettings', () => {
QUICK_PROMPTS_TAB,
SYSTEM_PROMPTS_TAB,
])('%s', (tab) => {
it('Opens the tab on button click', () => {
(useAssistantContext as jest.Mock).mockImplementation(() => ({
...mockContext,
selectedSettingsTab: tab === CONVERSATIONS_TAB ? ANONYMIZATION_TAB : CONVERSATIONS_TAB,
}));
const { getByTestId } = render(<AssistantSettings {...testProps} />, {
wrapper,
});
fireEvent.click(getByTestId(`${tab}-button`));
expect(setSelectedSettingsTab).toHaveBeenCalledWith(tab);
});
it('renders with the correct tab open', () => {
(useAssistantContext as jest.Mock).mockImplementation(() => ({
...mockContext,

View file

@ -9,14 +9,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiIcon,
EuiModal,
EuiModalFooter,
EuiKeyPadMenu,
EuiKeyPadMenuItem,
EuiPage,
EuiPageBody,
EuiPageSidebar,
EuiSplitPanel,
} from '@elastic/eui';
@ -80,16 +76,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
conversations,
conversationsLoaded,
}) => {
const {
assistantFeatures: {
assistantModelEvaluation: modelEvaluatorEnabled,
assistantKnowledgeBaseByDefault,
},
http,
toasts,
selectedSettingsTab,
setSelectedSettingsTab,
} = useAssistantContext();
const { http, toasts, selectedSettingsTab, setSelectedSettingsTab } = useAssistantContext();
useEffect(() => {
if (selectedSettingsTab == null) {
@ -214,115 +201,6 @@ export const AssistantSettings: React.FC<Props> = React.memo(
return (
<StyledEuiModal data-test-subj={TEST_IDS.SETTINGS_MODAL} onClose={onClose}>
<EuiPage paddingSize="none">
{!assistantKnowledgeBaseByDefault && (
<EuiPageSidebar
paddingSize="xs"
css={css`
min-inline-size: unset !important;
max-width: 104px;
`}
>
<EuiKeyPadMenu>
<EuiKeyPadMenuItem
id={CONVERSATIONS_TAB}
label={i18n.CONVERSATIONS_MENU_ITEM}
isSelected={!selectedSettingsTab || selectedSettingsTab === CONVERSATIONS_TAB}
onClick={() => setSelectedSettingsTab(CONVERSATIONS_TAB)}
data-test-subj={`${CONVERSATIONS_TAB}-button`}
>
<>
<EuiIcon
type="editorComment"
size="xl"
css={css`
position: relative;
top: -10px;
`}
/>
<EuiIcon
type="editorComment"
size="l"
css={css`
position: relative;
transform: rotateY(180deg);
top: -7px;
`}
/>
</>
</EuiKeyPadMenuItem>
<EuiKeyPadMenuItem
id={QUICK_PROMPTS_TAB}
label={i18n.QUICK_PROMPTS_MENU_ITEM}
isSelected={selectedSettingsTab === QUICK_PROMPTS_TAB}
onClick={() => setSelectedSettingsTab(QUICK_PROMPTS_TAB)}
data-test-subj={`${QUICK_PROMPTS_TAB}-button`}
>
<>
<EuiIcon type="editorComment" size="xxl" />
<EuiIcon
type="bolt"
size="s"
color="warning"
css={css`
position: absolute;
top: 11px;
left: 14px;
`}
/>
</>
</EuiKeyPadMenuItem>
<EuiKeyPadMenuItem
id={SYSTEM_PROMPTS_TAB}
label={i18n.SYSTEM_PROMPTS_MENU_ITEM}
isSelected={selectedSettingsTab === SYSTEM_PROMPTS_TAB}
onClick={() => setSelectedSettingsTab(SYSTEM_PROMPTS_TAB)}
data-test-subj={`${SYSTEM_PROMPTS_TAB}-button`}
>
<EuiIcon type="editorComment" size="xxl" />
<EuiIcon
type="storage"
size="s"
color="success"
css={css`
position: absolute;
top: 11px;
left: 14px;
`}
/>
</EuiKeyPadMenuItem>
<EuiKeyPadMenuItem
id={ANONYMIZATION_TAB}
label={i18n.ANONYMIZATION_MENU_ITEM}
isSelected={selectedSettingsTab === ANONYMIZATION_TAB}
onClick={() => setSelectedSettingsTab(ANONYMIZATION_TAB)}
data-test-subj={`${ANONYMIZATION_TAB}-button`}
>
<EuiIcon type="eyeClosed" size="l" />
</EuiKeyPadMenuItem>
<EuiKeyPadMenuItem
id={KNOWLEDGE_BASE_TAB}
label={i18n.KNOWLEDGE_BASE_MENU_ITEM}
isSelected={selectedSettingsTab === KNOWLEDGE_BASE_TAB}
onClick={() => setSelectedSettingsTab(KNOWLEDGE_BASE_TAB)}
data-test-subj={`${KNOWLEDGE_BASE_TAB}-button`}
>
<EuiIcon type="notebookApp" size="l" />
</EuiKeyPadMenuItem>
{modelEvaluatorEnabled && (
<EuiKeyPadMenuItem
id={EVALUATION_TAB}
label={i18n.EVALUATION_MENU_ITEM}
isSelected={selectedSettingsTab === EVALUATION_TAB}
onClick={() => setSelectedSettingsTab(EVALUATION_TAB)}
data-test-subj={`${EVALUATION_TAB}-button`}
>
<EuiIcon type="crossClusterReplicationApp" size="l" />
</EuiKeyPadMenuItem>
)}
</EuiKeyPadMenu>
</EuiPageSidebar>
)}
<EuiPageBody paddingSize="none" panelled={true}>
<EuiSplitPanel.Outer grow={true}>
<EuiSplitPanel.Inner

View file

@ -9,9 +9,8 @@ import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
import { AssistantSettingsButton } from './assistant_settings_button';
import { AssistantSettingsModal } from './assistant_settings_modal';
import { welcomeConvo } from '../../mock/conversation';
import { CONVERSATIONS_TAB } from './const';
const setIsSettingsModalVisible = jest.fn();
const onConversationSelected = jest.fn();
@ -54,21 +53,14 @@ jest.mock('./assistant_settings', () => ({
),
}));
describe('AssistantSettingsButton', () => {
describe('AssistantSettingsModal', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('Clicking the settings gear opens the conversations tab', () => {
const { getByTestId } = render(<AssistantSettingsButton {...testProps} />);
fireEvent.click(getByTestId('settings'));
expect(setSelectedSettingsTab).toHaveBeenCalledWith(CONVERSATIONS_TAB);
expect(setIsSettingsModalVisible).toHaveBeenCalledWith(true);
});
it('Settings modal is visible and calls correct actions per click', () => {
const { getByTestId } = render(
<AssistantSettingsButton {...testProps} isSettingsModalVisible />
<AssistantSettingsModal {...testProps} isSettingsModalVisible />
);
fireEvent.click(getByTestId('on-close'));
expect(setIsSettingsModalVisible).toHaveBeenCalledWith(false);

View file

@ -6,7 +6,6 @@
*/
import React, { useCallback } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { QueryObserverResult, RefetchOptions, RefetchQueryFilters } from '@tanstack/react-query';
import { DataStreamApis } from '../use_data_stream_apis';
import { AIConnector } from '../../connectorland/connector_selector';
@ -14,7 +13,6 @@ import { Conversation } from '../../..';
import { AssistantSettings } from './assistant_settings';
import * as i18n from './translations';
import { useAssistantContext } from '../../assistant_context';
import { CONVERSATIONS_TAB } from './const';
interface Props {
defaultConnector?: AIConnector;
@ -32,12 +30,11 @@ interface Props {
}
/**
* Gear button that opens the assistant settings modal
* Assistant settings modal
*/
export const AssistantSettingsButton: React.FC<Props> = React.memo(
export const AssistantSettingsModal: React.FC<Props> = React.memo(
({
defaultConnector,
isDisabled = false,
isSettingsModalVisible,
setIsSettingsModalVisible,
selectedConversationId,
@ -47,11 +44,7 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
refetchCurrentUserConversations,
refetchPrompts,
}) => {
const {
assistantFeatures: { assistantKnowledgeBaseByDefault },
toasts,
setSelectedSettingsTab,
} = useAssistantContext();
const { toasts } = useAssistantContext();
// Modal control functions
const cleanupAndCloseModal = useCallback(() => {
@ -79,41 +72,20 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
[cleanupAndCloseModal, refetchCurrentUserConversations, refetchPrompts, toasts]
);
const handleShowConversationSettings = useCallback(() => {
setSelectedSettingsTab(CONVERSATIONS_TAB);
setIsSettingsModalVisible(true);
}, [setIsSettingsModalVisible, setSelectedSettingsTab]);
return (
<>
{!assistantKnowledgeBaseByDefault && (
<EuiToolTip position="right" content={i18n.SETTINGS_TOOLTIP}>
<EuiButtonIcon
aria-label={i18n.SETTINGS}
data-test-subj="settings"
onClick={handleShowConversationSettings}
isDisabled={isDisabled}
iconType="gear"
size="xs"
color="text"
/>
</EuiToolTip>
)}
{isSettingsModalVisible && (
<AssistantSettings
defaultConnector={defaultConnector}
selectedConversationId={selectedConversationId}
onConversationSelected={onConversationSelected}
onClose={handleCloseModal}
onSave={handleSave}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
/>
)}
</>
isSettingsModalVisible && (
<AssistantSettings
defaultConnector={defaultConnector}
selectedConversationId={selectedConversationId}
onConversationSelected={onConversationSelected}
onClose={handleCloseModal}
onSave={handleSave}
conversations={conversations}
conversationsLoaded={conversationsLoaded}
/>
)
);
}
);
AssistantSettingsButton.displayName = 'AssistantSettingsButton';
AssistantSettingsModal.displayName = 'AssistantSettingsModal';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { ReactElement, useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -32,11 +32,7 @@ interface Params {
export const SettingsContextMenu: React.FC<Params> = React.memo(
({ isDisabled = false, onChatCleared }: Params) => {
const {
navigateToApp,
knowledgeBase,
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
} = useAssistantContext();
const { navigateToApp, knowledgeBase } = useAssistantContext();
const [isPopoverOpen, setPopover] = useState(false);
@ -91,12 +87,11 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
closePopover();
}, [closePopover, showAlertSettingsModal]);
// We are migrating away from the settings modal in favor of the new Stack Management UI
// Currently behind `assistantKnowledgeBaseByDefault` FF
const newItems: ReactElement[] = useMemo(
const items = useMemo(
() => [
<EuiContextMenuItem
aria-label={'ai-assistant-settings'}
key={'ai-assistant-settings'}
onClick={handleNavigateToSettings}
icon={'gear'}
data-test-subj={'ai-assistant-settings'}
@ -105,6 +100,7 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={'knowledge-base'}
key={'knowledge-base'}
onClick={handleNavigateToKnowledgeBase}
icon={'documents'}
data-test-subj={'knowledge-base'}
@ -113,6 +109,7 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={'anonymization'}
key={'anonymization'}
onClick={handleNavigateToAnonymization}
icon={'eye'}
data-test-subj={'anonymization'}
@ -121,6 +118,7 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
</EuiContextMenuItem>,
<EuiContextMenuItem
aria-label={'alerts-to-analyze'}
key={'alerts-to-analyze'}
onClick={handleShowAlertsModal}
icon={'magnifyWithExclamation'}
data-test-subj={'alerts-to-analyze'}
@ -134,21 +132,9 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
</EuiFlexItem>
</EuiFlexGroup>
</EuiContextMenuItem>,
],
[
handleNavigateToAnonymization,
handleNavigateToKnowledgeBase,
handleNavigateToSettings,
handleShowAlertsModal,
knowledgeBase.latestAlerts,
]
);
const items = useMemo(
() => [
...(enableKnowledgeBaseByDefault ? newItems : []),
<EuiContextMenuItem
aria-label={'clear-chat'}
key={'clear-chat'}
onClick={showDestroyModal}
icon={'refresh'}
data-test-subj={'clear-chat'}
@ -160,7 +146,14 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
</EuiContextMenuItem>,
],
[enableKnowledgeBaseByDefault, newItems, showDestroyModal]
[
handleNavigateToAnonymization,
handleNavigateToKnowledgeBase,
handleNavigateToSettings,
handleShowAlertsModal,
knowledgeBase.latestAlerts,
showDestroyModal,
]
);
const handleReset = useCallback(() => {

View file

@ -47,15 +47,6 @@ const defaultProps = {
},
setUpdatedKnowledgeBaseSettings,
};
const mockDelete = jest.fn();
jest.mock('../assistant/api/knowledge_base/use_delete_knowledge_base', () => ({
useDeleteKnowledgeBase: jest.fn(() => {
return {
mutate: mockDelete,
isLoading: false,
};
}),
}));
const mockSetup = jest.fn();
jest.mock('../assistant/api/knowledge_base/use_setup_knowledge_base', () => ({

View file

@ -32,7 +32,6 @@ const mockContext = {
http: {
get: jest.fn(),
},
assistantFeatures: { assistantKnowledgeBaseByDefault: true },
selectedSettingsTab: null,
assistantAvailability: {
isAssistantEnabled: true,
@ -175,17 +174,6 @@ describe('KnowledgeBaseSettingsManagement', () => {
isLoading: false,
});
});
it('renders old kb settings when enableKnowledgeBaseByDefault is not enabled', () => {
(useAssistantContext as jest.Mock).mockImplementation(() => ({
...mockContext,
assistantFeatures: {
assistantKnowledgeBaseByDefault: false,
},
}));
render(<KnowledgeBaseSettingsManagement dataViews={mockDataViews} />, { wrapper });
expect(screen.getByTestId('knowledge-base-settings')).toBeInTheDocument();
});
it('renders loading spinner when data is not fetched', () => {
(useKnowledgeBaseStatus as jest.Mock).mockReturnValue({ data: {}, isFetched: false });
render(<KnowledgeBaseSettingsManagement dataViews={mockDataViews} />, {

View file

@ -48,7 +48,6 @@ import { Flyout } from '../../assistant/common/components/assistant_settings_man
import { useFlyoutModalVisibility } from '../../assistant/common/components/assistant_settings_management/flyout/use_flyout_modal_visibility';
import { IndexEntryEditor } from './index_entry_editor';
import { DocumentEntryEditor } from './document_entry_editor';
import { KnowledgeBaseSettings } from '../knowledge_base_settings';
import { SetupKnowledgeBaseButton } from '../setup_knowledge_base_button';
import { useDeleteKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_delete_knowledge_base_entries';
import {
@ -73,7 +72,6 @@ interface Params {
export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ dataViews }) => {
const {
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
assistantAvailability: { hasManageGlobalKnowledgeBase, isAssistantEnabled },
http,
toasts,
@ -162,7 +160,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
} = useKnowledgeBaseEntries({
http,
toasts,
enabled: enableKnowledgeBaseByDefault && isAssistantEnabled,
enabled: isAssistantEnabled,
isRefetching: kbStatus?.is_setup_in_progress,
});
@ -332,21 +330,6 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
}
}, [createEntry, duplicateKBItem, resetStateAndCloseFlyout]);
if (!enableKnowledgeBaseByDefault) {
return (
<>
<KnowledgeBaseSettings
knowledgeBase={knowledgeBase}
setUpdatedKnowledgeBaseSettings={handleUpdateKnowledgeBaseSettings}
/>
<AssistantSettingsBottomBar
hasPendingChanges={hasPendingChanges}
onCancelClick={onCancelClick}
onSaveButtonClicked={onSaveButtonClicked}
/>
</>
);
}
return (
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">

View file

@ -35,9 +35,6 @@ describe('Attack discovery tour', () => {
jest.clearAllMocks();
(useAssistantContext as jest.Mock).mockReturnValue({
navigateToApp,
assistantFeatures: {
assistantKnowledgeBaseByDefault: true,
},
});
jest.mocked(useLocalStorage).mockReturnValue([
{
@ -68,25 +65,6 @@ describe('Attack discovery tour', () => {
expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull();
});
it('should not render any tour steps when knowledge base feature flag is not activated', () => {
(useAssistantContext as jest.Mock).mockReturnValue({
navigateToApp,
assistantFeatures: {
assistantKnowledgeBaseByDefault: false,
},
});
render(
<KnowledgeBaseTour>
<h1>{'Hello world'}</h1>
</KnowledgeBaseTour>,
{
wrapper: TestProviders,
}
);
expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull();
expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull();
});
it('should not render any tour steps when tour is on step 2 and page is not knowledge base', () => {
jest.mocked(useLocalStorage).mockReturnValue([
{

View file

@ -28,10 +28,7 @@ const KnowledgeBaseTourComp: React.FC<{
children?: EuiTourStepProps['children'];
isKbSettingsPage?: boolean;
}> = ({ children, isKbSettingsPage = false }) => {
const {
navigateToApp,
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
} = useAssistantContext();
const { navigateToApp } = useAssistantContext();
const [tourState, setTourState] = useLocalStorage<TourState>(
NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE,
@ -106,7 +103,7 @@ const KnowledgeBaseTourComp: React.FC<{
return () => clearTimeout(timer);
}, []);
if (!enableKnowledgeBaseByDefault || isTestAutomation || !tourState?.isTourActive) {
if (isTestAutomation || !tourState?.isTourActive) {
return children ?? null;
}

View file

@ -107,6 +107,7 @@ export const getAssistantSubFeaturesMap = (
): Map<AssistantSubFeatureId, SubFeatureConfig> => {
const assistantSubFeaturesList: Array<[AssistantSubFeatureId, SubFeatureConfig]> = [
[AssistantSubFeatureId.updateAnonymization, updateAnonymizationSubFeature],
[AssistantSubFeatureId.manageGlobalKnowledgeBase, manageGlobalKnowledgeBaseSubFeature],
];
// Use the following code to add feature based on feature flag
@ -114,13 +115,6 @@ export const getAssistantSubFeaturesMap = (
// assistantSubFeaturesList.push([AssistantSubFeatureId.featureId, featureSubFeature]);
// }
if (experimentalFeatures.assistantKnowledgeBaseByDefault) {
assistantSubFeaturesList.push([
AssistantSubFeatureId.manageGlobalKnowledgeBase,
manageGlobalKnowledgeBaseSubFeature,
]);
}
const assistantSubFeaturesMap = new Map<AssistantSubFeatureId, SubFeatureConfig>(
assistantSubFeaturesList
);

View file

@ -1,71 +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 type { MsearchQueryBody } from '../lib/langchain/elasticsearch_store/helpers/get_msearch_query_body';
/**
* This mock Elasticsearch msearch request body contains two queries:
* - The first query is a similarity (vector) search
* - The second query is a required KB document (terms) search
*/
export const mSearchQueryBody: MsearchQueryBody = {
body: [
{
index: '.kibana-elastic-ai-assistant-kb',
},
{
query: {
bool: {
must_not: [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
],
must: [
{
semantic: {
field: 'semantic_text',
query:
'Generate an ESQL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.',
},
},
],
},
},
size: 1,
},
{
index: '.kibana-elastic-ai-assistant-kb',
},
{
query: {
bool: {
must: [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
],
},
},
size: 1,
},
],
};

View file

@ -1,101 +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 type { MsearchResponse } from '@elastic/elasticsearch/lib/api/types';
/**
* This mock response from an Elasticsearch msearch contains two hits, where
* the first hit is from a similarity (vector) search, and the second hit is a
* required KB document (terms) search.
*/
export const mockMsearchResponse: MsearchResponse = {
took: 142,
responses: [
{
took: 142,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 129,
relation: 'eq',
},
max_score: 21.658352,
hits: [
{
_index: '.kibana-elastic-ai-assistant-kb',
_id: 'fa1c8ba1-25c9-4404-9736-09b7eb7124f8',
_score: 21.658352,
_ignored: ['text.keyword'],
_source: {
metadata: {
source:
'/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc',
},
vector: {
tokens: {
wild: 1.2001507,
// truncated for mock
},
model_id: '.elser_model_2',
},
text: "[[esql-from]]\n=== `FROM`\n\nThe `FROM` source command returns a table with up to 10,000 documents from a\ndata stream, index, or alias. Each row in the resulting table represents a\ndocument. Each column corresponds to a field, and can be accessed by the name\nof that field.\n\n[source,esql]\n----\nFROM employees\n----\n\nYou can use <<api-date-math-index-names,date math>> to refer to indices, aliases\nand data streams. This can be useful for time series data, for example to access\ntoday's index:\n\n[source,esql]\n----\nFROM <logs-{now/d}>\n----\n\nUse comma-separated lists or wildcards to query multiple data streams, indices,\nor aliases:\n\n[source,esql]\n----\nFROM employees-00001,employees-*\n----\n",
},
},
],
},
status: 200,
},
{
took: 3,
timed_out: false,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
hits: {
total: {
value: 14,
relation: 'eq',
},
max_score: 0.034783483,
hits: [
{
_index: '.kibana-elastic-ai-assistant-kb',
_id: '280d4882-0f64-4471-a268-669a3f8c958f',
_score: 0.034783483,
_ignored: ['text.keyword'],
_source: {
metadata: {
source:
'/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/example_queries/esql_example_query_0001.asciidoc',
required: true,
kbResource: 'esql',
},
vector: {
tokens: {
user: 1.1084619,
// truncated for mock
},
model_id: '.elser_model_2',
},
text: '[[esql-example-queries]]\n\nThe following is an example an ES|QL query:\n\n```\nFROM logs-*\n| WHERE NOT CIDR_MATCH(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")\n| STATS destcount = COUNT(destination.ip) by user.name, host.name\n| ENRICH ldap_lookup_new ON user.name\n| WHERE group.name IS NOT NULL\n| EVAL follow_up = CASE(\n destcount >= 100, "true",\n "false")\n| SORT destcount desc\n| KEEP destcount, host.name, user.name, group.name, follow_up\n```\n',
},
},
],
},
status: 200,
},
],
};

File diff suppressed because one or more lines are too long

View file

@ -67,13 +67,6 @@ export const getPostKnowledgeBaseRequest = (resource?: string) =>
query: { resource },
});
export const getDeleteKnowledgeBaseRequest = (resource?: string) =>
requestMock.create({
method: 'delete',
path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL,
query: { resource },
});
export const getGetCapabilitiesRequest = () =>
requestMock.create({
method: 'get',

View file

@ -127,11 +127,11 @@ const createElasticAssistantRequestContextMock = (
() => clients.elasticAssistant.getAIAssistantKnowledgeBaseDataClient
) as unknown as jest.MockInstance<
Promise<AIAssistantKnowledgeBaseDataClient | null>,
[params: GetAIAssistantKnowledgeBaseDataClientParams],
[params?: GetAIAssistantKnowledgeBaseDataClientParams],
unknown
> &
((
params: GetAIAssistantKnowledgeBaseDataClientParams
params?: GetAIAssistantKnowledgeBaseDataClientParams
) => Promise<AIAssistantKnowledgeBaseDataClient | null>),
getCurrentUser: jest.fn(),
getServerBasePath: jest.fn(),

View file

@ -1,28 +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 type { Field, FieldValue, QueryDslTermQuery } from '@elastic/elasticsearch/lib/api/types';
/**
* These (mock) terms may be used in multiple queries.
*
* For example, it may be be used in a vector search to exclude the required `esql` KB docs.
*
* It may also be used in a terms search to find all of the required `esql` KB docs.
*/
export const mockTerms: Array<Partial<Record<Field, QueryDslTermQuery | FieldValue>>> = [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
];

View file

@ -1,28 +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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
/**
* This Elasticsearch query DSL is a terms search for required `esql` KB docs
*/
export const mockTermsSearchQuery: QueryDslQueryContainer = {
bool: {
must: [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
],
},
};

View file

@ -1,37 +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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
/**
* A mock vector search query DSL
*/
export const mockVectorSearchQuery: QueryDslQueryContainer = {
bool: {
must_not: [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
],
must: [
{
semantic: {
field: 'semantic_text',
query:
'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called "follow_up" that contains a value of "true", otherwise, it should contain "false". The user names should also be enriched with their respective group names.',
},
},
],
},
} as QueryDslQueryContainer;

View file

@ -14,11 +14,9 @@ import {
} from '@kbn/core/server';
import {
DocumentEntryCreateFields,
KnowledgeBaseEntryCreateProps,
KnowledgeBaseEntryResponse,
KnowledgeBaseEntryUpdateProps,
Metadata,
} from '@kbn/elastic-assistant-common';
import {
CREATE_KNOWLEDGE_BASE_ENTRY_ERROR_EVENT,
@ -33,9 +31,8 @@ export interface CreateKnowledgeBaseEntryParams {
logger: Logger;
spaceId: string;
user: AuthenticatedUser;
knowledgeBaseEntry: KnowledgeBaseEntryCreateProps | LegacyKnowledgeBaseEntryCreateProps;
knowledgeBaseEntry: KnowledgeBaseEntryCreateProps;
global?: boolean;
isV2?: boolean;
telemetry: AnalyticsServiceSetup;
}
@ -47,25 +44,16 @@ export const createKnowledgeBaseEntry = async ({
knowledgeBaseEntry,
logger,
global = false,
isV2 = false,
telemetry,
}: CreateKnowledgeBaseEntryParams): Promise<KnowledgeBaseEntryResponse | null> => {
const createdAt = new Date().toISOString();
const body = isV2
? transformToCreateSchema({
createdAt,
spaceId,
user,
entry: knowledgeBaseEntry as unknown as KnowledgeBaseEntryCreateProps,
global,
})
: transformToLegacyCreateSchema({
createdAt,
spaceId,
user,
entry: knowledgeBaseEntry as unknown as TransformToLegacyCreateSchemaProps['entry'],
global,
});
const body = transformToCreateSchema({
createdAt,
spaceId,
user,
entry: knowledgeBaseEntry as unknown as KnowledgeBaseEntryCreateProps,
global,
});
const telemetryPayload = {
entryType: body.type,
required: body.required ?? false,
@ -156,13 +144,7 @@ export const transformToUpdateSchema = ({
};
};
export const getUpdateScript = ({
entry,
isPatch,
}: {
entry: UpdateKnowledgeBaseEntrySchema;
isPatch?: boolean;
}) => {
export const getUpdateScript = ({ entry }: { entry: UpdateKnowledgeBaseEntrySchema }) => {
// Cannot use script for updating documents with semantic_text fields
return {
doc: {
@ -230,45 +212,3 @@ export const transformToCreateSchema = ({
semantic_text: entry.text,
};
};
export type LegacyKnowledgeBaseEntryCreateProps = Omit<
DocumentEntryCreateFields,
'kbResource' | 'source'
> & {
metadata: Metadata;
};
interface TransformToLegacyCreateSchemaProps {
createdAt: string;
spaceId: string;
user: AuthenticatedUser;
entry: LegacyKnowledgeBaseEntryCreateProps;
global?: boolean;
}
export const transformToLegacyCreateSchema = ({
createdAt,
spaceId,
user,
entry,
global = false,
}: TransformToLegacyCreateSchemaProps): CreateKnowledgeBaseEntrySchema => {
return {
'@timestamp': createdAt,
created_at: createdAt,
created_by: user.profile_uid ?? 'unknown',
updated_at: createdAt,
updated_by: user.profile_uid ?? 'unknown',
namespace: spaceId,
users: global
? []
: [
{
id: user.profile_uid,
name: user.username,
},
],
...entry,
vector: undefined,
};
};

View file

@ -9,89 +9,6 @@ import { FieldMap } from '@kbn/data-stream-adapter';
export const ASSISTANT_ELSER_INFERENCE_ID = 'elastic-security-ai-assistant-elser2';
export const knowledgeBaseFieldMap: FieldMap = {
'@timestamp': {
type: 'date',
array: false,
required: false,
},
id: {
type: 'keyword',
array: false,
required: true,
},
created_at: {
type: 'date',
array: false,
required: false,
},
created_by: {
type: 'keyword',
array: false,
required: false,
},
updated_at: {
type: 'date',
array: false,
required: false,
},
updated_by: {
type: 'keyword',
array: false,
required: false,
},
users: {
type: 'nested',
array: true,
required: false,
},
'users.id': {
type: 'keyword',
array: false,
required: true,
},
'users.name': {
type: 'keyword',
array: false,
required: false,
},
metadata: {
type: 'object',
array: false,
required: false,
},
'metadata.kbResource': {
type: 'keyword',
array: false,
required: false,
},
'metadata.required': {
type: 'boolean',
array: false,
required: false,
},
'metadata.source': {
type: 'keyword',
array: false,
required: false,
},
text: {
type: 'text',
array: false,
required: true,
},
vector: {
type: 'object',
array: false,
required: false,
},
'vector.tokens': {
type: 'rank_features',
array: false,
required: false,
},
} as const;
export const knowledgeBaseFieldMapV2: FieldMap = {
// Base fields
'@timestamp': {
type: 'date',

View file

@ -27,37 +27,29 @@ export const isModelAlreadyExistsError = (error: Error) => {
*
* @param filter - Optional filter to apply to the search
* @param kbResource - Specific resource tag to filter for, e.g. 'esql' or 'user'
* @param modelId - ID of the model to search with, e.g. `.elser_model_2`
* @param query - The search query provided by the user
* @param required - Whether to only include required entries
* @param user - The authenticated user
* @param v2KnowledgeBaseEnabled whether the new v2 KB is enabled
* @returns
*/
export const getKBVectorSearchQuery = ({
filter,
kbResource,
modelId,
query,
required,
user,
v2KnowledgeBaseEnabled = false,
}: {
filter?: QueryDslQueryContainer | undefined;
kbResource?: string | undefined;
modelId: string;
query?: string;
required?: boolean | undefined;
user: AuthenticatedUser;
v2KnowledgeBaseEnabled: boolean;
}): QueryDslQueryContainer => {
const kbResourceKey = v2KnowledgeBaseEnabled ? 'kb_resource' : 'metadata.kbResource';
const requiredKey = v2KnowledgeBaseEnabled ? 'required' : 'metadata.required';
const resourceFilter = kbResource
? [
{
term: {
[kbResourceKey]: kbResource,
kb_resource: kbResource,
},
},
]
@ -66,7 +58,7 @@ export const getKBVectorSearchQuery = ({
? [
{
term: {
[requiredKey]: required,
required,
},
},
]
@ -120,7 +112,7 @@ export const getKBVectorSearchQuery = ({
text_expansion: { 'vector.tokens': { model_id: string; model_text: string } };
}> = [];
if (v2KnowledgeBaseEnabled && query) {
if (query) {
semanticTextFilter = [
{
semantic: {
@ -129,17 +121,6 @@ export const getKBVectorSearchQuery = ({
},
},
];
} else if (!v2KnowledgeBaseEnabled) {
semanticTextFilter = [
{
text_expansion: {
'vector.tokens': {
model_id: modelId,
model_text: query as string,
},
},
},
];
}
return {

View file

@ -29,20 +29,11 @@ import { AnalyticsServiceSetup, ElasticsearchClient } from '@kbn/core/server';
import { IndexPatternsFetcher } from '@kbn/data-views-plugin/server';
import { map } from 'lodash';
import { AIAssistantDataClient, AIAssistantDataClientParams } from '..';
import { AssistantToolParams, GetElser } from '../../types';
import {
createKnowledgeBaseEntry,
LegacyKnowledgeBaseEntryCreateProps,
transformToCreateSchema,
transformToLegacyCreateSchema,
} from './create_knowledge_base_entry';
import { GetElser } from '../../types';
import { createKnowledgeBaseEntry, transformToCreateSchema } from './create_knowledge_base_entry';
import { EsDocumentEntry, EsIndexEntry, EsKnowledgeBaseEntrySchema } from './types';
import { transformESSearchToKnowledgeBaseEntry } from './transforms';
import {
ESQL_DOCS_LOADED_QUERY,
SECURITY_LABS_RESOURCE,
USER_RESOURCE,
} from '../../routes/knowledge_base/constants';
import { SECURITY_LABS_RESOURCE, USER_RESOURCE } from '../../routes/knowledge_base/constants';
import {
getKBVectorSearchQuery,
getStructuredToolForIndexEntry,
@ -61,7 +52,6 @@ import { ASSISTANT_ELSER_INFERENCE_ID } from './field_maps_configuration';
*/
export interface GetAIAssistantKnowledgeBaseDataClientParams {
modelIdOverride?: string;
v2KnowledgeBaseEnabled?: boolean;
manageGlobalKnowledgeBaseAIAssistant?: boolean;
}
@ -71,7 +61,6 @@ interface KnowledgeBaseDataClientParams extends AIAssistantDataClientParams {
getIsKBSetupInProgress: () => boolean;
ingestPipelineResourceName: string;
setIsKBSetupInProgress: (isInProgress: boolean) => void;
v2KnowledgeBaseEnabled: boolean;
manageGlobalKnowledgeBaseAIAssistant: boolean;
}
export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
@ -82,11 +71,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
public get isSetupInProgress() {
return this.options.getIsKBSetupInProgress();
}
public get isV2KnowledgeBaseEnabled() {
return this.options.v2KnowledgeBaseEnabled;
}
/**
* Returns whether setup of the Knowledge Base can be performed (essentially an ML features check)
*
@ -150,70 +134,39 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
};
/**
* Deploy the ELSER model with default configuration
*/
private deployModel = async () => {
const elserId = await this.options.getElserId();
this.options.logger.debug(`Deploying ELSER model '${elserId}'...`);
try {
const esClient = await this.options.elasticsearchClientPromise;
await esClient.ml.startTrainedModelDeployment({
model_id: elserId,
wait_for: 'fully_allocated',
});
} catch (error) {
this.options.logger.error(`Error deploying ELSER model '${elserId}':\n${error}`);
throw new Error(`Error deploying ELSER model '${elserId}':\n${error}`);
}
};
/**
* Checks if the provided model is deployed and allocated in Elasticsearch
* Checks if the inference endpoint is deployed and allocated in Elasticsearch
*
* @returns Promise<boolean> indicating whether the model is deployed
*/
public isModelDeployed = async (): Promise<boolean> => {
const elserId = await this.options.getElserId();
this.options.logger.debug(`Checking if ELSER model '${elserId}' is deployed...`);
try {
if (this.isV2KnowledgeBaseEnabled) {
return await this.isInferenceEndpointExists();
} else {
const esClient = await this.options.elasticsearchClientPromise;
const getResponse = await esClient.ml.getTrainedModelsStats({
model_id: elserId,
});
// For standardized way of checking deployment status see: https://github.com/elastic/elasticsearch/issues/106986
const isReadyESS = (stats: MlTrainedModelStats) =>
stats.deployment_stats?.state === 'started' &&
stats.deployment_stats?.allocation_status.state === 'fully_allocated';
const isReadyServerless = (stats: MlTrainedModelStats) =>
(stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[])?.some(
(node) => node.routing_state.routing_state === 'started'
);
return getResponse.trained_model_stats?.some(
(stats) => isReadyESS(stats) || isReadyServerless(stats)
);
}
} catch (e) {
this.options.logger.debug(`Error checking if ELSER model '${elserId}' is deployed: ${e}`);
// Returns 404 if it doesn't exist
return false;
}
};
public isInferenceEndpointExists = async (): Promise<boolean> => {
try {
const esClient = await this.options.elasticsearchClientPromise;
return !!(await esClient.inference.get({
const inferenceExists = !!(await esClient.inference.get({
inference_id: ASSISTANT_ELSER_INFERENCE_ID,
task_type: 'sparse_embedding',
}));
if (!inferenceExists) {
return false;
}
const elserId = await this.options.getElserId();
const getResponse = await esClient.ml.getTrainedModelsStats({
model_id: elserId,
});
// For standardized way of checking deployment status see: https://github.com/elastic/elasticsearch/issues/106986
const isReadyESS = (stats: MlTrainedModelStats) =>
stats.deployment_stats?.state === 'started' &&
stats.deployment_stats?.allocation_status.state === 'fully_allocated';
const isReadyServerless = (stats: MlTrainedModelStats) =>
(stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[])?.some(
(node) => node.routing_state.routing_state === 'started'
);
return getResponse.trained_model_stats?.some(
(stats) => isReadyESS(stats) || isReadyServerless(stats)
);
} catch (error) {
this.options.logger.debug(
`Error checking if Inference endpoint ${ASSISTANT_ELSER_INFERENCE_ID} exists: ${error}`
@ -227,25 +180,24 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
this.options.logger.debug(`Deploying ELSER model '${elserId}'...`);
try {
const esClient = await this.options.elasticsearchClientPromise;
if (this.isV2KnowledgeBaseEnabled) {
await esClient.inference.put({
task_type: 'sparse_embedding',
inference_id: ASSISTANT_ELSER_INFERENCE_ID,
inference_config: {
service: 'elasticsearch',
service_settings: {
adaptive_allocations: {
enabled: true,
min_number_of_allocations: 0,
max_number_of_allocations: 8,
},
num_threads: 1,
model_id: elserId,
await esClient.inference.put({
task_type: 'sparse_embedding',
inference_id: ASSISTANT_ELSER_INFERENCE_ID,
inference_config: {
service: 'elasticsearch',
service_settings: {
adaptive_allocations: {
enabled: true,
min_number_of_allocations: 0,
max_number_of_allocations: 8,
},
task_settings: {},
num_threads: 1,
model_id: elserId,
},
});
}
task_settings: {},
},
});
} catch (error) {
this.options.logger.error(
`Error creating inference endpoint for ELSER model '${elserId}':\n${error}`
@ -268,11 +220,9 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
*/
public setupKnowledgeBase = async ({
soClient,
v2KnowledgeBaseEnabled = true,
ignoreSecurityLabs = false,
}: {
soClient: SavedObjectsClientContract;
v2KnowledgeBaseEnabled?: boolean;
ignoreSecurityLabs?: boolean;
}): Promise<void> => {
if (this.options.getIsKBSetupInProgress()) {
@ -284,40 +234,38 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
this.options.setIsKBSetupInProgress(true);
const elserId = await this.options.getElserId();
if (v2KnowledgeBaseEnabled) {
// Delete legacy ESQL knowledge base docs if they exist, and silence the error if they do not
try {
const esClient = await this.options.elasticsearchClientPromise;
const legacyESQL = await esClient.deleteByQuery({
index: this.indexTemplateAndPattern.alias,
query: {
bool: {
must: [{ terms: { 'metadata.kbResource': ['esql', 'unknown'] } }],
},
// Delete legacy ESQL knowledge base docs if they exist, and silence the error if they do not
try {
const esClient = await this.options.elasticsearchClientPromise;
const legacyESQL = await esClient.deleteByQuery({
index: this.indexTemplateAndPattern.alias,
query: {
bool: {
must: [{ terms: { 'metadata.kbResource': ['esql', 'unknown'] } }],
},
});
if (legacyESQL?.total != null && legacyESQL?.total > 0) {
this.options.logger.info(
`Removed ${legacyESQL?.total} ESQL knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.`
);
}
// Delete any existing Security Labs content
const securityLabsDocs = await esClient.deleteByQuery({
index: this.indexTemplateAndPattern.alias,
query: {
bool: {
must: [{ terms: { kb_resource: [SECURITY_LABS_RESOURCE] } }],
},
},
});
if (securityLabsDocs?.total) {
this.options.logger.info(
`Removed ${securityLabsDocs?.total} Security Labs knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.`
);
}
} catch (e) {
this.options.logger.info('No legacy ESQL or Security Labs knowledge base docs to delete');
},
});
if (legacyESQL?.total != null && legacyESQL?.total > 0) {
this.options.logger.info(
`Removed ${legacyESQL?.total} ESQL knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.`
);
}
// Delete any existing Security Labs content
const securityLabsDocs = await esClient.deleteByQuery({
index: this.indexTemplateAndPattern.alias,
query: {
bool: {
must: [{ terms: { kb_resource: [SECURITY_LABS_RESOURCE] } }],
},
},
});
if (securityLabsDocs?.total) {
this.options.logger.info(
`Removed ${securityLabsDocs?.total} Security Labs knowledge base docs from knowledge base data stream: ${this.indexTemplateAndPattern.alias}.`
);
}
} catch (e) {
this.options.logger.info('No legacy ESQL or Security Labs knowledge base docs to delete');
}
try {
@ -336,39 +284,22 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
this.options.logger.debug(`ELSER model '${elserId}' is already installed`);
}
if (!this.isV2KnowledgeBaseEnabled) {
const isDeployed = await this.isModelDeployed();
if (!isDeployed) {
await this.deployModel();
await pRetry(
async () =>
(await this.isModelDeployed())
? Promise.resolve()
: Promise.reject(new Error('Model not deployed')),
{ minTimeout: 2000, retries: 10 }
);
this.options.logger.debug(`ELSER model '${elserId}' successfully deployed!`);
} else {
this.options.logger.debug(`ELSER model '${elserId}' is already deployed`);
}
} else {
const inferenceExists = await this.isInferenceEndpointExists();
if (!inferenceExists) {
await this.createInferenceEndpoint();
const inferenceExists = await this.isInferenceEndpointExists();
if (!inferenceExists) {
await this.createInferenceEndpoint();
this.options.logger.debug(
`Inference endpoint for ELSER model '${elserId}' successfully deployed!`
);
} else {
this.options.logger.debug(
`Inference endpoint for ELSER model '${elserId}' is already deployed`
);
}
this.options.logger.debug(
`Inference endpoint for ELSER model '${elserId}' successfully deployed!`
);
} else {
this.options.logger.debug(
`Inference endpoint for ELSER model '${elserId}' is already deployed`
);
}
this.options.logger.debug(`Checking if Knowledge Base docs have been loaded...`);
if (v2KnowledgeBaseEnabled && !ignoreSecurityLabs) {
if (!ignoreSecurityLabs) {
const labsDocsLoaded = await this.isSecurityLabsDocsLoaded();
if (!labsDocsLoaded) {
this.options.logger.debug(`Loading Security Labs KB docs...`);
@ -415,39 +346,20 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
const { errors, docs_created: docsCreated } = await writer.bulk({
documentsToCreate: documents.map((doc) => {
// v1 schema has metadata nested in a `metadata` object
if (this.options.v2KnowledgeBaseEnabled) {
return transformToCreateSchema({
createdAt: changedAt,
spaceId: this.spaceId,
user: authenticatedUser,
entry: {
type: DocumentEntryType.value,
name: 'unknown',
text: doc.pageContent,
kbResource: doc.metadata.kbResource ?? 'unknown',
required: doc.metadata.required ?? false,
source: doc.metadata.source ?? 'unknown',
},
global,
});
} else {
return transformToLegacyCreateSchema({
createdAt: changedAt,
spaceId: this.spaceId,
user: authenticatedUser,
entry: {
type: DocumentEntryType.value,
name: 'unknown',
text: doc.pageContent,
metadata: {
kbResource: doc.metadata.kbResource ?? 'unknown',
required: doc.metadata.required ?? false,
source: doc.metadata.source ?? 'unknown',
},
},
global,
});
}
return transformToCreateSchema({
createdAt: changedAt,
spaceId: this.spaceId,
user: authenticatedUser,
entry: {
type: DocumentEntryType.value,
name: 'unknown',
text: doc.pageContent,
kbResource: doc.metadata.kbResource ?? 'unknown',
required: doc.metadata.required ?? false,
source: doc.metadata.source ?? 'unknown',
},
global,
});
}),
authenticatedUser,
});
@ -467,18 +379,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
return created?.data ? transformESSearchToKnowledgeBaseEntry(created?.data) : [];
};
/**
* Returns if ES|QL KB docs have been loaded
*/
public isESQLDocsLoaded = async (): Promise<boolean> => {
const esqlDocs = await this.getKnowledgeBaseDocumentEntries({
query: ESQL_DOCS_LOADED_QUERY,
// kbResource, // Note: `8.15` installs have kbResource as `unknown`, so don't filter yet
required: true,
});
return esqlDocs.length > 0;
};
/**
* Returns if user's KB docs exists
*/
@ -492,15 +392,12 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
}
const esClient = await this.options.elasticsearchClientPromise;
const modelId = await this.options.getElserId();
try {
const vectorSearchQuery = getKBVectorSearchQuery({
kbResource: USER_RESOURCE,
required: false,
user,
modelId,
v2KnowledgeBaseEnabled: this.options.v2KnowledgeBaseEnabled,
});
const result = await esClient.search<EsDocumentEntry>({
@ -531,15 +428,12 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
const expectedDocsCount = await getSecurityLabsDocsCount({ logger: this.options.logger });
const esClient = await this.options.elasticsearchClientPromise;
const modelId = await this.options.getElserId();
try {
const vectorSearchQuery = getKBVectorSearchQuery({
kbResource: SECURITY_LABS_RESOURCE,
required: false,
user,
modelId,
v2KnowledgeBaseEnabled: this.options.v2KnowledgeBaseEnabled,
});
const result = await esClient.search<EsDocumentEntry>({
@ -585,7 +479,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
}
const esClient = await this.options.elasticsearchClientPromise;
const modelId = await this.options.getElserId();
const vectorSearchQuery = getKBVectorSearchQuery({
filter,
@ -593,8 +486,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
query,
required,
user,
modelId,
v2KnowledgeBaseEnabled: this.options.v2KnowledgeBaseEnabled,
});
try {
@ -605,14 +496,11 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
});
const results = result.hits.hits.map((hit) => {
const metadata = this.options.v2KnowledgeBaseEnabled
? {
source: hit?._source?.source,
required: hit?._source?.required,
kbResource: hit?._source?.kb_resource,
}
: // @ts-ignore v1 schema has metadata nested in a `metadata` object and kbResource vs kb_resource
hit?._source?.metadata ?? {};
const metadata = {
source: hit?._source?.source,
required: hit?._source?.required,
kbResource: hit?._source?.kb_resource,
};
return new Document({
pageContent: hit?._source?.text ?? '',
metadata,
@ -691,7 +579,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
telemetry,
global = false,
}: {
knowledgeBaseEntry: KnowledgeBaseEntryCreateProps | LegacyKnowledgeBaseEntryCreateProps;
knowledgeBaseEntry: KnowledgeBaseEntryCreateProps;
global?: boolean;
telemetry: AnalyticsServiceSetup;
}): Promise<KnowledgeBaseEntryResponse | null> => {
@ -721,7 +609,6 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
knowledgeBaseEntry,
global,
telemetry,
isV2: this.options.v2KnowledgeBaseEnabled,
});
};
@ -732,10 +619,8 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
* is scoped to system user.
*/
public getAssistantTools = async ({
assistantToolParams,
esClient,
}: {
assistantToolParams: AssistantToolParams;
esClient: ElasticsearchClient;
}): Promise<StructuredTool[]> => {
const user = this.options.currentUser;
@ -746,9 +631,7 @@ export class AIAssistantKnowledgeBaseDataClient extends AIAssistantDataClient {
}
try {
const elserId = this.isV2KnowledgeBaseEnabled
? ASSISTANT_ELSER_INFERENCE_ID
: await this.options.getElserId();
const elserId = ASSISTANT_ELSER_INFERENCE_ID;
const userFilter = getKBUserFilter(user);
const results = await this.findDocuments<EsIndexEntry>({
// Note: This is a magic number to set some upward bound as to not blow the context with too

View file

@ -5,31 +5,8 @@
* 2.0.
*/
export const knowledgeBaseIngestPipeline = ({
id,
modelId,
v2KnowledgeBaseEnabled,
}: {
id: string;
modelId: string;
v2KnowledgeBaseEnabled: boolean;
}) => ({
export const knowledgeBaseIngestPipeline = ({ id }: { id: string }) => ({
id,
description: 'Embedding pipeline for Elastic AI Assistant ELSER Knowledge Base',
processors: !v2KnowledgeBaseEnabled
? [
{
inference: {
if: 'ctx?.text != null',
model_id: modelId,
input_output: [
{
input_field: 'text',
output_field: 'vector.tokens',
},
],
},
},
]
: [],
processors: [],
});

View file

@ -53,8 +53,6 @@ export const pipelineExists = async ({ esClient, id }: PipelineExistsParams): Pr
interface CreatePipelineParams {
esClient: ElasticsearchClient;
id: string;
modelId: string;
v2KnowledgeBaseEnabled: boolean;
}
/**
@ -63,22 +61,14 @@ interface CreatePipelineParams {
* @param params params
* @param params.esClient Elasticsearch client with privileges to check for ingest pipelines
* @param params.id ID of the ingest pipeline
* @param params.modelId ID of the ELSER model
*
* @returns Promise<boolean> indicating whether the pipeline was created
*/
export const createPipeline = async ({
esClient,
id,
modelId,
v2KnowledgeBaseEnabled,
}: CreatePipelineParams): Promise<boolean> => {
export const createPipeline = async ({ esClient, id }: CreatePipelineParams): Promise<boolean> => {
try {
const response = await esClient.ingest.putPipeline(
knowledgeBaseIngestPipeline({
id,
modelId,
v2KnowledgeBaseEnabled,
})
);

View file

@ -27,10 +27,7 @@ import { conversationsFieldMap } from '../ai_assistant_data_clients/conversation
import { assistantPromptsFieldMap } from '../ai_assistant_data_clients/prompts/field_maps_configuration';
import { assistantAnonymizationFieldsFieldMap } from '../ai_assistant_data_clients/anonymization_fields/field_maps_configuration';
import { AIAssistantDataClient } from '../ai_assistant_data_clients';
import {
knowledgeBaseFieldMap,
knowledgeBaseFieldMapV2,
} from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration';
import { knowledgeBaseFieldMap } from '../ai_assistant_data_clients/knowledge_base/field_maps_configuration';
import {
AIAssistantKnowledgeBaseDataClient,
GetAIAssistantKnowledgeBaseDataClientParams,
@ -85,8 +82,6 @@ export class AIAssistantService {
private resourceInitializationHelper: ResourceInstallationHelper;
private initPromise: Promise<InitializationPromise>;
private isKBSetupInProgress: boolean = false;
// Temporary 'feature flag' to determine if we should initialize the new kb mappings, toggled when accessing kbDataClient
private v2KnowledgeBaseEnabled: boolean = false;
private hasInitializedV2KnowledgeBase: boolean = false;
constructor(private readonly options: AIAssistantServiceOpts) {
@ -156,7 +151,7 @@ export class AIAssistantService {
// Apply `default_pipeline` if pipeline exists for resource
...(resource in this.resourceNames.pipelines &&
// Remove this param and initialization when the `assistantKnowledgeBaseByDefault` feature flag is removed
!(resource === 'knowledgeBase' && this.v2KnowledgeBaseEnabled)
!(resource === 'knowledgeBase')
? {
template: {
settings: {
@ -185,16 +180,6 @@ export class AIAssistantService {
pluginStop$: this.options.pluginStop$,
});
// If v2 is enabled, re-install data stream resources for new mappings
if (this.v2KnowledgeBaseEnabled) {
this.options.logger.debug(`Using V2 Knowledge Base Mappings`);
this.knowledgeBaseDataStream = this.createDataStream({
resource: 'knowledgeBase',
kibanaVersion: this.options.kibanaVersion,
fieldMap: knowledgeBaseFieldMapV2,
});
}
await this.knowledgeBaseDataStream.install({
esClient,
logger: this.options.logger,
@ -206,28 +191,18 @@ export class AIAssistantService {
esClient,
id: this.resourceNames.pipelines.knowledgeBase,
});
// TODO: When FF is removed, ensure pipeline is re-created for those upgrading
if (
// Install for v1
(!this.v2KnowledgeBaseEnabled && !pipelineCreated) ||
// Upgrade from v1 to v2
(pipelineCreated && this.v2KnowledgeBaseEnabled)
) {
// ensure pipeline is re-created for those upgrading
// pipeline is noop now, so if one does not exist we do not need one
if (pipelineCreated) {
this.options.logger.debug(
`Installing ingest pipeline - ${this.resourceNames.pipelines.knowledgeBase}`
);
const response = await createPipeline({
esClient,
id: this.resourceNames.pipelines.knowledgeBase,
modelId: await this.getElserId(),
v2KnowledgeBaseEnabled: this.v2KnowledgeBaseEnabled,
});
this.options.logger.debug(`Installed ingest pipeline: ${response}`);
} else {
this.options.logger.debug(
`Ingest pipeline already exists - ${this.resourceNames.pipelines.knowledgeBase}`
);
}
await this.promptsDataStream.install({
@ -363,25 +338,16 @@ export class AIAssistantService {
opts: CreateAIAssistantClientParams & GetAIAssistantKnowledgeBaseDataClientParams
): Promise<AIAssistantKnowledgeBaseDataClient | null> {
// If modelIdOverride is set, swap getElserId(), and ensure the pipeline is re-created with the correct model
if (opts.modelIdOverride != null) {
if (opts?.modelIdOverride != null) {
const modelIdOverride = opts.modelIdOverride;
this.getElserId = async () => modelIdOverride;
}
// Note: Due to plugin lifecycle and feature flag registration timing, we need to pass in the feature flag here
// Remove this param and initialization when the `assistantKnowledgeBaseByDefault` feature flag is removed
if (opts.v2KnowledgeBaseEnabled) {
this.v2KnowledgeBaseEnabled = true;
}
// If either v2 KB or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure
// If a V2 KnowledgeBase has never been initialized or a modelIdOverride is provided, we need to reinitialize all persistence resources to make sure
// they're using the correct model/mappings. Technically all existing KB data is stale since it was created
// with a different model/mappings, but modelIdOverride is only intended for testing purposes at this time
// Added hasInitializedV2KnowledgeBase to prevent the console noise from re-init on each KB request
if (
!this.hasInitializedV2KnowledgeBase &&
(opts.v2KnowledgeBaseEnabled || opts.modelIdOverride != null)
) {
if (!this.hasInitializedV2KnowledgeBase || opts?.modelIdOverride != null) {
await this.initializeResources();
this.hasInitializedV2KnowledgeBase = true;
}
@ -404,7 +370,6 @@ export class AIAssistantService {
ml: this.options.ml,
setIsKBSetupInProgress: this.setIsKBSetupInProgress.bind(this),
spaceId: opts.spaceId,
v2KnowledgeBaseEnabled: opts.v2KnowledgeBaseEnabled ?? false,
manageGlobalKnowledgeBaseAIAssistant: opts.manageGlobalKnowledgeBaseAIAssistant ?? false,
});
}

View file

@ -1,408 +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 { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
import {
IndicesCreateResponse,
MlGetTrainedModelsStatsResponse,
} from '@elastic/elasticsearch/lib/api/types';
import { Document } from 'langchain/document';
import {
ElasticsearchStore,
FALLBACK_SIMILARITY_SEARCH_SIZE,
TERMS_QUERY_SIZE,
} from './elasticsearch_store';
import { mockMsearchResponse } from '../../../__mocks__/msearch_response';
import { mockQueryText } from '../../../__mocks__/query_text';
import { coreMock } from '@kbn/core/server/mocks';
import {
KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT,
KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT,
} from '../../telemetry/event_based_telemetry';
import { Metadata } from '@kbn/elastic-assistant-common';
jest.mock('uuid', () => ({
v4: jest.fn(),
}));
jest.mock('@kbn/core/server', () => ({
ElasticsearchClient: jest.fn(),
Logger: jest.fn().mockImplementation(() => ({
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
})),
}));
const mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
const mockLogger = loggingSystemMock.createLogger();
const reportEvent = jest.fn();
const mockTelemetry = { ...coreMock.createSetup().analytics, reportEvent };
const KB_INDEX = '.elastic-assistant-kb';
const getElasticsearchStore = () => {
return new ElasticsearchStore(mockEsClient, KB_INDEX, mockLogger, mockTelemetry);
};
describe('ElasticsearchStore', () => {
let esStore: ElasticsearchStore;
beforeEach(() => {
esStore = getElasticsearchStore();
jest.clearAllMocks();
});
describe('Index Management', () => {
it('Checks if index exists', async () => {
mockEsClient.indices.exists.mockResolvedValue(true);
const exists = await esStore.indexExists();
expect(exists).toBe(true);
expect(mockEsClient.indices.exists).toHaveBeenCalledWith({ index: KB_INDEX });
});
it('Creates an index', async () => {
mockEsClient.indices.create.mockResolvedValue({
acknowledged: true,
} as IndicesCreateResponse);
const created = await esStore.createIndex();
expect(created).toBe(true);
expect(mockEsClient.indices.create).toHaveBeenCalledWith({
index: KB_INDEX,
mappings: {
properties: {
metadata: {
properties: {
kbResource: { type: 'keyword' },
required: { type: 'boolean' },
source: { type: 'keyword' },
},
},
vector: { properties: { tokens: { type: 'rank_features' } } },
},
},
settings: { default_pipeline: '.kibana-elastic-ai-assistant-kb-ingest-pipeline' },
});
});
it('Deletes an index', async () => {
mockEsClient.indices.delete.mockResolvedValue({ acknowledged: true });
const deleted = await esStore.deleteIndex();
expect(deleted).toBe(true);
expect(mockEsClient.indices.delete).toHaveBeenCalledWith({ index: KB_INDEX });
});
});
describe('Pipeline Management', () => {
it('Checks if pipeline exists', async () => {
mockEsClient.ingest.getPipeline.mockResolvedValue({});
const exists = await esStore.pipelineExists();
expect(exists).toBe(false);
expect(mockEsClient.ingest.getPipeline).toHaveBeenCalledWith({
id: '.kibana-elastic-ai-assistant-kb-ingest-pipeline',
});
});
it('Creates an ingest pipeline', async () => {
mockEsClient.ingest.putPipeline.mockResolvedValue({ acknowledged: true });
const created = await esStore.createPipeline();
expect(created).toBe(true);
expect(mockEsClient.ingest.putPipeline).toHaveBeenCalledWith({
description: 'Embedding pipeline for Elastic AI Assistant ELSER Knowledge Base',
id: '.kibana-elastic-ai-assistant-kb-ingest-pipeline',
processors: [
{
inference: {
field_map: { text: 'text_field' },
inference_config: { text_expansion: { results_field: 'tokens' } },
model_id: '.elser_model_2',
target_field: 'vector',
},
},
],
});
});
it('Deletes an ingest pipeline', async () => {
mockEsClient.ingest.deletePipeline.mockResolvedValue({ acknowledged: true });
const deleted = await esStore.deletePipeline();
expect(deleted).toBe(true);
expect(mockEsClient.ingest.deletePipeline).toHaveBeenCalledWith({
id: '.kibana-elastic-ai-assistant-kb-ingest-pipeline',
});
});
});
describe('isModelInstalled', () => {
it('returns true if model is started and fully allocated', async () => {
mockEsClient.ml.getTrainedModelsStats.mockResolvedValue({
trained_model_stats: [
{
deployment_stats: {
state: 'started',
allocation_status: {
state: 'fully_allocated',
},
},
},
],
} as MlGetTrainedModelsStatsResponse);
const isInstalled = await esStore.isModelInstalled('.elser_model_2');
expect(isInstalled).toBe(true);
expect(mockEsClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({
model_id: '.elser_model_2',
});
});
it('returns false if model is not started', async () => {
mockEsClient.ml.getTrainedModelsStats.mockResolvedValue({
trained_model_stats: [
{
deployment_stats: {
state: 'starting',
allocation_status: {
state: 'fully_allocated',
},
},
},
],
} as MlGetTrainedModelsStatsResponse);
const isInstalled = await esStore.isModelInstalled('.elser_model_2');
expect(isInstalled).toBe(false);
expect(mockEsClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({
model_id: '.elser_model_2',
});
});
it('returns false if model is not fully allocated', async () => {
mockEsClient.ml.getTrainedModelsStats.mockResolvedValue({
trained_model_stats: [
{
deployment_stats: {
state: 'started',
allocation_status: {
state: 'starting',
},
},
},
],
} as MlGetTrainedModelsStatsResponse);
const isInstalled = await esStore.isModelInstalled('.elser_model_2');
expect(isInstalled).toBe(false);
expect(mockEsClient.ml.getTrainedModelsStats).toHaveBeenCalledWith({
model_id: '.elser_model_2',
});
});
});
describe('addDocuments', () => {
it('Checks if documents are added', async () => {
mockEsClient.bulk.mockResolvedValue({
errors: false,
took: 515,
ingest_took: 4026,
items: [
{
index: {
_index: '.kibana-elastic-ai-assistant-kb',
_id: 'be2584a9-ad2e-4f13-a11c-c0b79423079c',
_version: 2,
result: 'updated',
forced_refresh: true,
_shards: {
total: 2,
successful: 1,
failed: 0,
},
_seq_no: 1,
_primary_term: 1,
status: 200,
},
},
],
});
const document = new Document<Metadata>({
pageContent: 'interesting stuff',
metadata: { kbResource: 'esql', required: false, source: '1' },
});
const docsInstalled = await esStore.addDocuments([document]);
expect(docsInstalled).toStrictEqual(['be2584a9-ad2e-4f13-a11c-c0b79423079c']);
expect(mockEsClient.bulk).toHaveBeenCalledWith({
operations: [
{
index: {
_id: undefined,
_index: '.elastic-assistant-kb',
},
},
{
metadata: {
kbResource: 'esql',
required: false,
source: '1',
},
text: 'interesting stuff',
},
],
refresh: true,
});
});
});
describe('similaritySearch', () => {
it('Checks if documents are found', async () => {
mockEsClient.msearch.mockResolvedValue(mockMsearchResponse);
const searchResults = await esStore.similaritySearch(mockQueryText);
expect(searchResults).toStrictEqual([
{
pageContent:
"[[esql-from]]\n=== `FROM`\n\nThe `FROM` source command returns a table with up to 10,000 documents from a\ndata stream, index, or alias. Each row in the resulting table represents a\ndocument. Each column corresponds to a field, and can be accessed by the name\nof that field.\n\n[source,esql]\n----\nFROM employees\n----\n\nYou can use <<api-date-math-index-names,date math>> to refer to indices, aliases\nand data streams. This can be useful for time series data, for example to access\ntoday's index:\n\n[source,esql]\n----\nFROM <logs-{now/d}>\n----\n\nUse comma-separated lists or wildcards to query multiple data streams, indices,\nor aliases:\n\n[source,esql]\n----\nFROM employees-00001,employees-*\n----\n",
metadata: {
source:
'/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc',
},
},
{
pageContent:
'[[esql-example-queries]]\n\nThe following is an example an ES|QL query:\n\n```\nFROM logs-*\n| WHERE NOT CIDR_MATCH(destination.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")\n| STATS destcount = COUNT(destination.ip) by user.name, host.name\n| ENRICH ldap_lookup_new ON user.name\n| WHERE group.name IS NOT NULL\n| EVAL follow_up = CASE(\n destcount >= 100, "true",\n "false")\n| SORT destcount desc\n| KEEP destcount, host.name, user.name, group.name, follow_up\n```\n',
metadata: {
source:
'/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/example_queries/esql_example_query_0001.asciidoc',
},
},
]);
expect(mockEsClient.msearch).toHaveBeenCalledWith({
body: [
{
index: '.elastic-assistant-kb',
},
{
query: {
bool: {
must_not: [{ term: { 'metadata.required': true } }],
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text: mockQueryText,
},
},
},
],
},
},
size: FALLBACK_SIMILARITY_SEARCH_SIZE, // <-- `FALLBACK_SIMILARITY_SEARCH_SIZE` is used when `k` is not provided
},
{
index: '.elastic-assistant-kb',
},
{
query: {
bool: {
must: [{ term: { 'metadata.required': true } }],
},
},
size: TERMS_QUERY_SIZE,
},
],
});
});
it('uses the value of `k` instead of the `FALLBACK_SIMILARITY_SEARCH_SIZE` when `k` is provided', async () => {
mockEsClient.msearch.mockResolvedValue(mockMsearchResponse);
const k = 4;
await esStore.similaritySearch(mockQueryText, k);
expect(mockEsClient.msearch).toHaveBeenCalledWith({
body: [
{
index: '.elastic-assistant-kb',
},
{
query: {
bool: {
must_not: [{ term: { 'metadata.required': true } }],
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text: mockQueryText,
},
},
},
],
},
},
size: k, // <-- `k` is used instead of `FALLBACK_SIMILARITY_SEARCH_SIZE`
},
{
index: '.elastic-assistant-kb',
},
{
query: {
bool: {
must: [{ term: { 'metadata.required': true } }],
},
},
size: TERMS_QUERY_SIZE,
},
],
});
});
it('Reports successful telemetry event', async () => {
mockEsClient.msearch.mockResolvedValue(mockMsearchResponse);
await esStore.similaritySearch(mockQueryText);
expect(reportEvent).toHaveBeenCalledWith(KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT.eventType, {
model: '.elser_model_2',
responseTime: 142,
resultCount: 2,
});
});
it('Reports error telemetry event', async () => {
mockEsClient.msearch.mockRejectedValue(new Error('Oh no!'));
await esStore.similaritySearch(mockQueryText);
expect(reportEvent).toHaveBeenCalledWith(KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT.eventType, {
model: '.elser_model_2',
errorMessage: 'Oh no!',
});
});
});
});

View file

@ -1,478 +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 { type AnalyticsServiceSetup, ElasticsearchClient, Logger } from '@kbn/core/server';
import {
MappingTypeMapping,
MlTrainedModelDeploymentNodesStats,
MlTrainedModelStats,
} from '@elastic/elasticsearch/lib/api/types';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Callbacks } from '@langchain/core/callbacks/manager';
import { Document } from 'langchain/document';
import { VectorStore } from '@langchain/core/vectorstores';
import * as uuid from 'uuid';
import { Metadata } from '@kbn/elastic-assistant-common';
import { transformError } from '@kbn/securitysolution-es-utils';
import { ElasticsearchEmbeddings } from '../embeddings/elasticsearch_embeddings';
import { FlattenedHit, getFlattenedHits } from './helpers/get_flattened_hits';
import { getMsearchQueryBody } from './helpers/get_msearch_query_body';
import { getTermsSearchQuery } from './helpers/get_terms_search_query';
import { getVectorSearchQuery } from './helpers/get_vector_search_query';
import type { MsearchResponse } from './helpers/types';
import {
KNOWLEDGE_BASE_INDEX_PATTERN,
KNOWLEDGE_BASE_INGEST_PIPELINE,
} from '../../../routes/knowledge_base/constants';
import { getRequiredKbDocsTermsQueryDsl } from './helpers/get_required_kb_docs_terms_query_dsl';
import {
KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT,
KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT,
} from '../../telemetry/event_based_telemetry';
import { AIAssistantKnowledgeBaseDataClient } from '../../../ai_assistant_data_clients/knowledge_base';
interface CreatePipelineParams {
id?: string;
description?: string;
}
interface CreateIndexParams {
index?: string;
pipeline?: string;
}
/**
* A fallback for the query `size` that determines how many documents to
* return from Elasticsearch when performing a similarity search.
*
* The size is typically determined by the implementation of LangChain's
* `VectorStoreRetriever._getRelevantDocuments` function, so this fallback is
* only required when using the `ElasticsearchStore` directly.
*/
export const FALLBACK_SIMILARITY_SEARCH_SIZE = 10;
/** The maximum number of hits to return from a `terms` query, via the `size` parameter */
export const TERMS_QUERY_SIZE = 10000;
/**
* Basic ElasticsearchStore implementation only leveraging ELSER for storage and retrieval.
*/
export class ElasticsearchStore extends VectorStore {
declare FilterType: QueryDslQueryContainer;
private readonly esClient: ElasticsearchClient;
private readonly kbDataClient: AIAssistantKnowledgeBaseDataClient | undefined;
private readonly index: string;
private readonly logger: Logger;
private readonly telemetry: AnalyticsServiceSetup;
private readonly model: string;
private kbResource?: string;
_vectorstoreType(): string {
return 'elasticsearch';
}
constructor(
esClient: ElasticsearchClient,
index: string,
logger: Logger,
telemetry: AnalyticsServiceSetup,
model?: string,
kbResource?: string | undefined,
kbDataClient?: AIAssistantKnowledgeBaseDataClient
) {
super(new ElasticsearchEmbeddings(logger), { esClient, index });
this.esClient = esClient;
this.index = index ?? KNOWLEDGE_BASE_INDEX_PATTERN;
this.logger = logger;
this.telemetry = telemetry;
this.model = model ?? '.elser_model_2';
this.kbResource = kbResource;
this.kbDataClient = kbDataClient;
}
setKbResource(kbResource: string) {
this.kbResource = kbResource;
}
/**
* Add documents to the store. Embeddings are created on ingest into index configured with
* ELSER ingest pipeline. Returns a list of document IDs.
*
* @param documents Documents to add to the store
* @param options Any additional options as defined in the interface
* @returns Promise<string[]> of document IDs added to the store
*/
addDocuments = async (
documents: Array<Document<Metadata>>,
options?: Record<string, never>
): Promise<string[]> => {
// Code path for when `assistantKnowledgeBaseByDefault` FF is enabled
// Once removed replace addDocuments() w/ addDocumentsViaDataClient()
if (this.kbDataClient != null) {
return this.addDocumentsViaDataClient(documents, options);
}
const pipelineExists = await this.pipelineExists();
if (!pipelineExists) {
await this.createPipeline();
}
const operations = documents.flatMap(({ pageContent, metadata }) => [
{ index: { _index: this.index, _id: uuid.v4() } },
{ text: pageContent, metadata },
]);
try {
const response = await this.esClient.bulk({ refresh: true, operations });
this.logger.debug(() => `Add Documents Response:\n ${JSON.stringify(response)}`);
const errorIds = response.items.filter((i) => i.index?.error != null);
operations.forEach((op, i) => {
if (errorIds.some((e) => e.index?._id === op.index?._id)) {
this.logger.error(`Error adding document to KB: ${JSON.stringify(operations?.[i + 1])}`);
}
});
return response.items.flatMap((i) =>
i.index?._id != null && i.index.error == null ? [i.index._id] : []
);
} catch (e) {
this.logger.error(`Error loading data into KB\n ${e}`);
return [];
}
};
addDocumentsViaDataClient = async (
documents: Array<Document<Metadata>>,
options?: Record<string, never>
): Promise<string[]> => {
if (!this.kbDataClient) {
this.logger.error('No kbDataClient provided');
return [];
}
try {
const response = await this.kbDataClient.addKnowledgeBaseDocuments({
documents,
global: true,
});
return response.map((doc) => doc.id);
} catch (e) {
this.logger.error(`Error loading data into KB\n ${e}`);
return [];
}
};
/**
* Add vectors to the store. Returns a list of document IDs.
*
* @param vectors Vector representation of documents to add to the store
* @param documents Documents corresponding to the provided vectors
* @param options Any additional options as defined in the interface
* @returns Promise<string[]> of document IDs added to the store
*/
addVectors = (
vectors: number[][],
documents: Document[],
options?: {}
): Promise<string[] | void> => {
// Note: implement if/when needed
this.logger.info('ElasticsearchStore.addVectors not implemented');
return Promise.resolve(undefined);
};
/**
* Performs similarity search on the store using the provided query vector and filter, returning k similar
* documents along with their score.
*
* @param query Query vector to search with
* @param k Number of similar documents to return
* @param filter Optional filter to apply to the search
*
* @returns Promise<Array<[Document, number]>> of similar documents and their scores
*/
similaritySearchVectorWithScore = (
query: number[],
k: number,
filter?: this['FilterType']
): Promise<Array<[Document, number]>> => {
// Note: Implement if needed
this.logger.info('ElasticsearchStore.similaritySearchVectorWithScore not implemented');
return Promise.resolve([]);
};
// Non-abstract function overrides
/**
* Performs similarity search on the store using the provided query string and filter, returning k similar
* @param query Query vector to search with
* @param k Number of similar documents to return
* @param filter Optional filter to apply to the search
* @param _callbacks Optional callbacks
* @param filterRequiredDocs Optional whether or not to exclude the required docs filter
*
* Fun facts:
* - This function is called by LangChain's `VectorStoreRetriever._getRelevantDocuments`
* - The `k` parameter is typically determined by LangChain's `VectorStoreRetriever._getRelevantDocuments`, and has been observed to default to `4` in the wild (see langchain/dist/vectorstores/base.ts)
* @returns Promise<Document[]> of similar documents
*/
similaritySearch = async (
query: string,
k?: number,
filter?: this['FilterType'] | undefined,
_callbacks?: Callbacks | undefined,
filterRequiredDocs = true
): Promise<Document[]> => {
// requiredDocs is an array of filters that can be used in a `bool` Elasticsearch DSL query to filter in/out required KB documents:
const requiredDocs = filterRequiredDocs ? getRequiredKbDocsTermsQueryDsl(this.kbResource) : [];
// The `k` parameter is typically provided by LangChain's `VectorStoreRetriever._getRelevantDocuments`, which calls this function:
const vectorSearchQuerySize = k ?? FALLBACK_SIMILARITY_SEARCH_SIZE;
// build a vector search query:
const vectorSearchQuery = getVectorSearchQuery({
filter,
modelId: this.model,
mustNotTerms: requiredDocs,
query,
});
// build a (separate) terms search query:
const termsSearchQuery = getTermsSearchQuery(requiredDocs);
// combine the vector search query and the terms search queries into a single multi-search query:
const mSearchQueryBody = getMsearchQueryBody({
index: this.index,
termsSearchQuery,
termsSearchQuerySize: TERMS_QUERY_SIZE,
vectorSearchQuery,
vectorSearchQuerySize,
});
try {
// execute both queries via a single multi-search request:
const result = await this.esClient.msearch<MsearchResponse>(mSearchQueryBody);
// flatten the results of the combined queries into a single array of hits:
const results: FlattenedHit[] = result.responses.flatMap((response) => {
const maybeEsqlMsearchResponse: MsearchResponse = response as MsearchResponse;
return getFlattenedHits(maybeEsqlMsearchResponse);
});
this.telemetry.reportEvent(KNOWLEDGE_BASE_EXECUTION_SUCCESS_EVENT.eventType, {
model: this.model,
...(this.kbResource != null ? { resourceAccessed: this.kbResource } : {}),
resultCount: results.length,
responseTime: result.took ?? 0,
});
this.logger.debug(
() =>
`Similarity search metadata source:\n${JSON.stringify(
results.map((r) => r?.metadata?.source ?? '(missing metadata.source)'),
null,
2
)}`
);
return results;
} catch (e) {
const error = transformError(e);
this.telemetry.reportEvent(KNOWLEDGE_BASE_EXECUTION_ERROR_EVENT.eventType, {
model: this.model,
...(this.kbResource != null ? { resourceAccessed: this.kbResource } : {}),
errorMessage: error.message,
});
this.logger.error(e);
return [];
}
};
// ElasticsearchStore explicit utility functions
/**
* Checks if the provided index exists in Elasticsearch
*
* @returns Promise<boolean> indicating whether the index exists
* @param index Index to check
* @returns Promise<boolean> indicating whether the index exists
*/
indexExists = async (index?: string): Promise<boolean> => {
return this.esClient.indices.exists({ index: index ?? this.index });
};
/**
* Create index for ELSER embeddings in Elasticsearch
*
* @returns Promise<boolean> indicating whether the index was created
*/
createIndex = async ({ index, pipeline }: CreateIndexParams = {}): Promise<boolean> => {
const mappings: MappingTypeMapping = {
properties: {
metadata: {
properties: {
/** the category of knowledge, e.g. `esql` */
kbResource: { type: 'keyword' },
/** when `true`, return this document in all searches for the `kbResource` */
required: { type: 'boolean' },
/** often a file path when the document was created via a LangChain `DirectoryLoader`, this metadata describes the origin of the document */
source: { type: 'keyword' },
},
},
vector: {
properties: { tokens: { type: 'rank_features' } },
},
},
};
const settings = { default_pipeline: pipeline ?? KNOWLEDGE_BASE_INGEST_PIPELINE };
const response = await this.esClient.indices.create({
index: index ?? this.index,
mappings,
settings,
});
return response.acknowledged;
};
/**
* Delete index for ELSER embeddings in Elasticsearch
* @param index Index to delete, otherwise uses the default index
*
* @returns Promise<boolean> indicating whether the index was created
*/
deleteIndex = async (index?: string): Promise<boolean> => {
// Code path for when `assistantKnowledgeBaseByDefault` FF is enabled
// We won't be supporting delete operations for the KB data stream going forward, so this can be removed along with the FF
if (this.kbDataClient != null) {
const response = await this.esClient.indices.deleteDataStream({ name: index ?? this.index });
return response.acknowledged;
}
const response = await this.esClient.indices.delete({
index: index ?? this.index,
});
return response.acknowledged;
};
/**
* Checks if the provided ingest pipeline exists in Elasticsearch
*
* @param pipelineId ID of the ingest pipeline to check
* @returns Promise<boolean> indicating whether the pipeline exists
*/
pipelineExists = async (pipelineId?: string): Promise<boolean> => {
try {
const id =
pipelineId ??
this.kbDataClient?.options.ingestPipelineResourceName ??
KNOWLEDGE_BASE_INGEST_PIPELINE;
const response = await this.esClient.ingest.getPipeline({
id,
});
return Object.keys(response).length > 0;
} catch (e) {
// The GET /_ingest/pipeline/{pipelineId} API returns an empty object w/ 404 Not Found.
return false;
}
};
/**
* Create ingest pipeline for ELSER in Elasticsearch
*
* @returns Promise<boolean> indicating whether the pipeline was created
*/
createPipeline = async ({ id, description }: CreatePipelineParams = {}): Promise<boolean> => {
const response = await this.esClient.ingest.putPipeline({
id:
id ??
this.kbDataClient?.options.ingestPipelineResourceName ??
KNOWLEDGE_BASE_INGEST_PIPELINE,
description:
description ?? 'Embedding pipeline for Elastic AI Assistant ELSER Knowledge Base',
processors: [
{
inference: {
model_id: this.model,
target_field: 'vector',
field_map: {
text: 'text_field',
},
inference_config: {
// @ts-expect-error
text_expansion: {
results_field: 'tokens',
},
},
},
},
],
});
return response.acknowledged;
};
/**
* Delete ingest pipeline for ELSER in Elasticsearch
*
* @returns Promise<boolean> indicating whether the pipeline was created
*/
deletePipeline = async (pipelineId?: string): Promise<boolean> => {
const response = await this.esClient.ingest.deletePipeline({
id:
pipelineId ??
this.kbDataClient?.options.ingestPipelineResourceName ??
KNOWLEDGE_BASE_INGEST_PIPELINE,
});
return response.acknowledged;
};
/**
* Checks if the provided model is installed in Elasticsearch
*
* @param modelId ID of the model to check
* @returns Promise<boolean> indicating whether the model is installed
*/
async isModelInstalled(modelId?: string): Promise<boolean> {
try {
// Code path for when `assistantKnowledgeBaseByDefault` FF is enabled
if (this.kbDataClient != null) {
// esStore.isModelInstalled() is actually checking if the model is deployed, not installed, so do that instead
return this.kbDataClient.isModelDeployed();
}
const getResponse = await this.esClient.ml.getTrainedModelsStats({
model_id: modelId ?? this.model,
});
this.logger.debug(`modelId: ${modelId}`);
// For standardized way of checking deployment status see: https://github.com/elastic/elasticsearch/issues/106986
const isReadyESS = (stats: MlTrainedModelStats) =>
stats.deployment_stats?.state === 'started' &&
stats.deployment_stats?.allocation_status.state === 'fully_allocated';
const isReadyServerless = (stats: MlTrainedModelStats) =>
(stats.deployment_stats?.nodes as unknown as MlTrainedModelDeploymentNodesStats[]).some(
(node) => node.routing_state.routing_state === 'started'
);
return getResponse.trained_model_stats.some(
(stats) => isReadyESS(stats) || isReadyServerless(stats)
);
} catch (e) {
// Returns 404 if it doesn't exist
return false;
}
}
}

View file

@ -1,81 +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 { getFlattenedHits } from './get_flattened_hits';
import { mockMsearchResponse } from '../../../../__mocks__/msearch_response';
import type { MsearchResponse } from './types';
describe('getFlattenedHits', () => {
it('returns an empty array when the response is undefined', () => {
const result = getFlattenedHits(undefined);
expect(result).toEqual([]);
});
it('returns an empty array when hits > hits is empty', () => {
const result = getFlattenedHits({ hits: { hits: [] } });
expect(result).toEqual([]);
});
it('returns the expected flattened hits given a non-empty `MsearchResponse`', () => {
const expected = [
{
pageContent:
"[[esql-from]]\n=== `FROM`\n\nThe `FROM` source command returns a table with up to 10,000 documents from a\ndata stream, index, or alias. Each row in the resulting table represents a\ndocument. Each column corresponds to a field, and can be accessed by the name\nof that field.\n\n[source,esql]\n----\nFROM employees\n----\n\nYou can use <<api-date-math-index-names,date math>> to refer to indices, aliases\nand data streams. This can be useful for time series data, for example to access\ntoday's index:\n\n[source,esql]\n----\nFROM <logs-{now/d}>\n----\n\nUse comma-separated lists or wildcards to query multiple data streams, indices,\nor aliases:\n\n[source,esql]\n----\nFROM employees-00001,employees-*\n----\n",
metadata: {
source:
'/Users/andrew.goldstein/Projects/forks/andrew-goldstein/kibana/x-pack/plugins/elastic_assistant/server/knowledge_base/esql/documentation/source_commands/from.asciidoc',
},
},
];
const result = getFlattenedHits(mockMsearchResponse.responses[0] as MsearchResponse);
expect(result).toEqual(expected);
});
it('returns an array of FlattenedHits with empty strings when given an MsearchResponse with missing fields', () => {
const msearchResponse = {
hits: {
hits: [
{
_source: {
metadata: {
source: '/source/1',
},
},
},
{
_source: {
text: 'Source 2 text',
},
},
],
},
};
const expected = [
{
pageContent: '', // <-- missing text field
metadata: {
source: '/source/1',
},
},
{
pageContent: 'Source 2 text',
metadata: {
source: '', // <-- missing source field
},
},
];
const result = getFlattenedHits(msearchResponse);
expect(result).toEqual(expected);
});
});

View file

@ -1,37 +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 type { MsearchKbHit, MsearchResponse } from './types';
/**
* Represents a flattened hit from an Elasticsearch Msearch response
*
* It contains the page content and metadata source of a KB document
*/
export interface FlattenedHit {
pageContent: string;
metadata: {
source: string;
};
}
/**
* Returns an array of flattened hits from the specified Msearch response
* that contain the page content and metadata source of KB documents
*
* @param maybeMsearchResponse An Elasticsearch Msearch response, which returns the results of multiple searches in a single request
* @returns Returns an array of flattened hits from the specified Msearch response that contain the page content and metadata source of KB documents
*/
export const getFlattenedHits = (
maybeMsearchResponse: MsearchResponse | undefined
): FlattenedHit[] =>
maybeMsearchResponse?.hits?.hits?.flatMap((hit: MsearchKbHit) => ({
pageContent: hit?._source?.text ?? '',
metadata: {
source: hit?._source?.metadata?.source ?? '',
},
})) ?? [];

View file

@ -1,46 +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 { TERMS_QUERY_SIZE } from '../elasticsearch_store';
import { getMsearchQueryBody } from './get_msearch_query_body';
import { mockTermsSearchQuery } from '../../../../__mocks__/terms_search_query';
import { mockVectorSearchQuery } from '../../../../__mocks__/vector_search_query';
describe('getMsearchQueryBody', () => {
it('returns the expected multi-search request body', () => {
const index = '.kibana-elastic-ai-assistant-kb';
const vectorSearchQuery = mockVectorSearchQuery;
const vectorSearchQuerySize = 4;
const termsSearchQuery = mockTermsSearchQuery;
const termsSearchQuerySize = TERMS_QUERY_SIZE;
const result = getMsearchQueryBody({
index,
termsSearchQuery,
termsSearchQuerySize,
vectorSearchQuery,
vectorSearchQuerySize,
});
expect(result).toEqual({
body: [
{ index },
{
query: mockVectorSearchQuery,
size: vectorSearchQuerySize,
},
{ index },
{
query: mockTermsSearchQuery,
size: TERMS_QUERY_SIZE,
},
],
});
});
});

View file

@ -1,67 +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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
/**
* Represents an entry in a multi-search request body that specifies the name of an index to search
*/
export interface MsearchQueryBodyIndexEntry {
index: string;
}
/**
* Represents an entry in a multi-search request body that specifies a query to execute
*/
export interface MsearchQueryBodyQueryEntry {
query: QueryDslQueryContainer;
size: number;
}
/**
* Represents a multi-search request body, which returns the results of multiple searches in a single request
*/
export interface MsearchQueryBody {
body: Array<MsearchQueryBodyIndexEntry | MsearchQueryBodyQueryEntry>;
}
/**
* Returns a multi-search request body, which returns the results of multiple searches in a single request
*
* @param index The KB index to search, e.g. `.kibana-elastic-ai-assistant-kb`
* @param termsSearchQuery An Elasticsearch DSL query that performs a terms search, typically used to search for required KB documents
* @param termsSearchQuerySize The maximum number of required KB documents to return
* @param vectorSearchQuery An Elasticsearch DSL query that performs a vector search, typically used to search for similar KB documents
* @param vectorSearchQuerySize The maximum number of similar KB documents to return
* @returns A multi-search request body, which returns the results of multiple searches in a single request
*/
export const getMsearchQueryBody = ({
index,
termsSearchQuery,
termsSearchQuerySize,
vectorSearchQuery,
vectorSearchQuerySize,
}: {
index: string;
termsSearchQuery: QueryDslQueryContainer;
termsSearchQuerySize: number;
vectorSearchQuery: QueryDslQueryContainer;
vectorSearchQuerySize: number;
}): MsearchQueryBody => ({
body: [
{ index },
{
query: vectorSearchQuery,
size: vectorSearchQuerySize,
},
{ index },
{
query: termsSearchQuery,
size: termsSearchQuerySize,
},
],
});

View file

@ -1,21 +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 { getRequiredKbDocsTermsQueryDsl } from './get_required_kb_docs_terms_query_dsl';
const kbResource = 'esql';
describe('getRequiredKbDocsTermsQueryDsl', () => {
it('returns the expected terms query DSL', () => {
const result = getRequiredKbDocsTermsQueryDsl(kbResource);
expect(result).toEqual([
{ term: { 'metadata.kbResource': 'esql' } },
{ term: { 'metadata.required': true } },
]);
});
});

View file

@ -1,39 +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 type { Field, FieldValue, QueryDslTermQuery } from '@elastic/elasticsearch/lib/api/types';
/**
* For the specified topic, returns an array of filters that can be used in a
* `bool` Elasticsearch DSL query to filter in/out required KB documents.
*
* The returned filters can be used in different types of queries to, for example:
* - To filter out required KB documents from a vector search
* - To filter in required KB documents in a terms query
*
* @param kbResource Search for required KB documents for this topic
*
* @returns An array of `term`s that may be used in a `bool` Elasticsearch DSL query to filter in/out required KB documents
*/
export const getRequiredKbDocsTermsQueryDsl = (
kbResource?: string
): Array<Partial<Record<Field, QueryDslTermQuery | FieldValue>>> => [
...(kbResource != null
? [
{
term: {
'metadata.kbResource': kbResource,
},
},
]
: []),
{
term: {
'metadata.required': true,
},
},
];

View file

@ -1,21 +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 { getTermsSearchQuery } from './get_terms_search_query';
import { mockTerms } from '../../../../__mocks__/terms';
describe('getTermsSearchQuery', () => {
it('returns the expected Elasticsearch query DSL', () => {
const query = getTermsSearchQuery(mockTerms);
expect(query).toEqual({
bool: {
must: mockTerms,
},
});
});
});

View file

@ -1,29 +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 type {
Field,
FieldValue,
QueryDslTermQuery,
QueryDslQueryContainer,
} from '@elastic/elasticsearch/lib/api/types';
/**
* Returns an Elasticsearch DSL query that performs a terms search,
* such that all of the specified terms must be present in the search results.
*
* @param mustTerms All of the specified terms must be present in the search results
*
* @returns An Elasticsearch DSL query that performs a terms search, such that all of the specified terms must be present in the search results
*/
export const getTermsSearchQuery = (
mustTerms: Array<Partial<Record<Field, QueryDslTermQuery | FieldValue>>>
): QueryDslQueryContainer => ({
bool: {
must: [...mustTerms], // all of the specified terms must be present in the search results
},
});

View file

@ -1,129 +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 type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { getVectorSearchQuery } from './get_vector_search_query';
import { mockTerms } from '../../../../__mocks__/terms';
import { mockQueryText } from '../../../../__mocks__/query_text';
describe('getVectorSearchQuery', () => {
it('returns the expected query when mustNotTerms is empty', () => {
const result = getVectorSearchQuery({
filter: undefined,
modelId: '.elser_model_2',
mustNotTerms: [], // <--- empty
query: mockQueryText,
});
expect(result).toEqual({
bool: {
filter: undefined,
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text:
'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.',
},
},
},
],
must_not: [],
},
});
});
it('returns the expected query when mustNotTerms are provided', () => {
const result = getVectorSearchQuery({
filter: undefined,
modelId: '.elser_model_2',
mustNotTerms: mockTerms, // <--- mock terms
query: mockQueryText,
});
expect(result).toEqual({
bool: {
filter: undefined,
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text:
'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.',
},
},
},
],
must_not: [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
],
},
});
});
it('returns the expected results when a filter is provided', () => {
const filter: QueryDslQueryContainer = {
bool: {
must: [
{
term: {
'some.field': 'value',
},
},
],
},
};
const result = getVectorSearchQuery({
filter,
modelId: '.elser_model_2',
mustNotTerms: mockTerms, // <--- mock terms
query: mockQueryText,
});
expect(result).toEqual({
bool: {
filter,
must: [
{
text_expansion: {
'vector.tokens': {
model_id: '.elser_model_2',
model_text:
'Generate an ES|QL query that will count the number of connections made to external IP addresses, broken down by user. If the count is greater than 100 for a specific user, add a new field called follow_up that contains a value of true, otherwise, it should contain false. The user names should also be enriched with their respective group names.',
},
},
},
],
must_not: [
{
term: {
'metadata.kbResource': 'esql',
},
},
{
term: {
'metadata.required': true,
},
},
],
},
});
});
});

View file

@ -1,50 +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 type {
Field,
FieldValue,
QueryDslQueryContainer,
QueryDslTermQuery,
} from '@elastic/elasticsearch/lib/api/types';
/**
* Returns an Elasticsearch query DSL that performs a vector search
* that excludes a set of documents from the search results.
*
* @param filter Optional filter to apply to the search
* @param modelId ID of the model to search with, e.g. `.elser_model_2`
* @param mustNotTerms Array of objects that may be used in a `bool` Elasticsearch DSL query to, for example, exclude the required KB docs from the vector search, so there's no overlap
* @param query The search query provided by the user
* @returns
*/
export const getVectorSearchQuery = ({
filter,
modelId,
mustNotTerms,
query,
}: {
filter: QueryDslQueryContainer | undefined;
modelId: string;
mustNotTerms: Array<Partial<Record<Field, QueryDslTermQuery | FieldValue>>>;
query: string;
}): QueryDslQueryContainer => ({
bool: {
must_not: [...mustNotTerms],
must: [
{
text_expansion: {
'vector.tokens': {
model_id: modelId,
model_text: query,
},
},
},
],
filter,
},
});

View file

@ -1,48 +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.
*/
/**
* A hit from the response to an Elasticsearch multi-search request,
* which returns the results of multiple searches in a single request.
*
* Search hits may contain the following properties that may be present in
* knowledge base documents:
*
* 1) the `metadata` property, an object that may have the following properties:
* - `kbResource`: The name of the Knowledge Base resource that the document belongs to, e.g. `esql`
* - `required`: A boolean indicating whether the document is required for searches on the `kbResource` topic
* - `source`: Describes the origin of the document, sometimes a file path via a LangChain DirectoryLoader
* 2) the `text` property, a string containing the text of the document
* 3) the `vector` property, containing the document's embeddings
*/
export interface MsearchKbHit {
_id?: string;
_ignored?: string[];
_index?: string;
_score?: number;
_source?: {
metadata?: {
kbResource?: string;
required?: boolean;
source?: string;
};
text?: string;
vector?: {
tokens?: Record<string, number>;
};
};
}
/**
* A Response from an Elasticsearch multi-search request, which returns the
* results of multiple searches in a single request.
*/
export interface MsearchResponse {
hits?: {
hits?: MsearchKbHit[];
};
}

View file

@ -1,41 +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 { Embeddings, EmbeddingsParams } from '@langchain/core/embeddings';
import { Logger } from '@kbn/core/server';
/**
* Shell class for Elasticsearch embeddings as not needed in ElasticsearchStore since ELSER embeds on index
*/
export class ElasticsearchEmbeddings extends Embeddings {
private readonly logger: Logger;
constructor(logger: Logger, params?: EmbeddingsParams) {
super(params ?? {});
this.logger = logger;
}
/**
* TODO: Use inference API if not re-indexing to create embedding vectors, e.g.
*
* POST _ml/trained_models/.elser_model_2/_infer
* {
* "docs":[{"text_field": "The fool doth think he is wise, but the wise man knows himself to be a fool."}]
* }
*/
embedDocuments(documents: string[]): Promise<number[][]> {
// Note: implement if/when needed
this.logger.info('ElasticsearchEmbeddings.embedDocuments not implemented');
return Promise.resolve([]);
}
embedQuery(_: string): Promise<number[]> {
// Note: implement if/when needed
this.logger.info('ElasticsearchEmbeddings.embedQuery not implemented');
return Promise.resolve([]);
}
}

View file

@ -93,8 +93,9 @@ export const callAssistantGraph: AgentExecutor<true | false> = async ({
const latestMessage = langChainMessages.slice(-1); // the last message
// Check if KB is available
const isEnabledKnowledgeBase = (await dataClients?.kbDataClient?.isModelDeployed()) ?? false;
// Check if KB is available (not feature flag related)
const isEnabledKnowledgeBase =
(await dataClients?.kbDataClient?.isInferenceEndpointExists()) ?? false;
// Fetch any applicable tools that the source plugin may have registered
const assistantToolParams: AssistantToolParams = {
@ -118,9 +119,8 @@ export const callAssistantGraph: AgentExecutor<true | false> = async ({
);
// If KB enabled, fetch for any KB IndexEntries and generate a tool for each
if (isEnabledKnowledgeBase && dataClients?.kbDataClient?.isV2KnowledgeBaseEnabled) {
if (isEnabledKnowledgeBase) {
const kbTools = await dataClients?.kbDataClient?.getAssistantTools({
assistantToolParams,
esClient,
});
if (kbTools) {

View file

@ -86,7 +86,7 @@ const mockContext = {
indexTemplateAndPattern: {
alias: 'knowledge-base-alias',
},
isModelDeployed: jest.fn().mockResolvedValue(true),
isInferenceEndpointExists: jest.fn().mockResolvedValue(true),
}),
getAIAssistantAnonymizationFieldsDataClient: jest.fn().mockResolvedValue({
findDocuments: jest.fn().mockResolvedValue(getFindAnonymizationFieldsResultWithSingleHit()),

View file

@ -25,9 +25,7 @@ import { buildResponse } from '../../lib/build_response';
import {
appendAssistantMessageToConversation,
createConversationWithUserInput,
DEFAULT_PLUGIN_NAME,
getIsKnowledgeBaseInstalled,
getPluginNameFromRequest,
langChainExecute,
performChecks,
} from '../helpers';
@ -222,25 +220,14 @@ export const chatCompleteRoute = (
});
} catch (err) {
const error = transformError(err as Error);
const pluginName = getPluginNameFromRequest({
request,
defaultPluginName: DEFAULT_PLUGIN_NAME,
logger,
});
const v2KnowledgeBaseEnabled =
ctx.elasticAssistant.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault;
const kbDataClient =
(await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled,
})) ?? undefined;
(await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient()) ?? undefined;
const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient);
telemetry?.reportEvent(INVOKE_ASSISTANT_ERROR_EVENT.eventType, {
actionTypeId: actionTypeId ?? '',
model: request.body.model,
errorMessage: error.message,
// TODO rm actionTypeId check when llmClass for bedrock streaming is implemented
// tracked here: https://github.com/elastic/security-team/issues/7363
assistantStreamingEnabled: request.body.isStream ?? false,
isEnabledKnowledgeBase: isKnowledgeBaseInstalled,
});

View file

@ -33,7 +33,7 @@ import { omit } from 'lodash/fp';
import { buildResponse } from '../../lib/build_response';
import { AssistantDataClients } from '../../lib/langchain/executors/types';
import { AssistantToolParams, ElasticAssistantRequestHandlerContext, GetElser } from '../../types';
import { DEFAULT_PLUGIN_NAME, isV2KnowledgeBaseEnabled, performChecks } from '../helpers';
import { DEFAULT_PLUGIN_NAME, performChecks } from '../helpers';
import { fetchLangSmithDataset } from './utils';
import { transformESSearchToAnonymizationFields } from '../../ai_assistant_data_clients/anonymization_fields/helpers';
import { EsAnonymizationFieldsSchema } from '../../ai_assistant_data_clients/anonymization_fields/types';
@ -91,7 +91,6 @@ export const postEvaluateRoute = (
const actions = ctx.elasticAssistant.actions;
const logger = assistantContext.logger.get('evaluate');
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request });
// Perform license, authenticated user and evaluation FF checks
const checkResponse = performChecks({
@ -158,9 +157,7 @@ export const postEvaluateRoute = (
const conversationsDataClient =
(await assistantContext.getAIAssistantConversationsDataClient()) ?? undefined;
const kbDataClient =
(await assistantContext.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled,
})) ?? undefined;
(await assistantContext.getAIAssistantKnowledgeBaseDataClient()) ?? undefined;
const dataClients: AssistantDataClients = {
anonymizationFieldsDataClient,
conversationsDataClient,
@ -246,7 +243,7 @@ export const postEvaluateRoute = (
// Check if KB is available
const isEnabledKnowledgeBase =
(await dataClients.kbDataClient?.isModelDeployed()) ?? false;
(await dataClients.kbDataClient?.isInferenceEndpointExists()) ?? false;
// Skeleton request from route to pass to the agents
// params will be passed to the actions executor

View file

@ -374,8 +374,6 @@ export const langChainExecute = async ({
const assistantTools = assistantContext
.getRegisteredTools(pluginName)
.filter((x) => x.id !== 'attack-discovery'); // We don't (yet) support asking the assistant for NEW attack discoveries from a conversation
const v2KnowledgeBaseEnabled =
assistantContext.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault;
// get a scoped esClient for assistant memory
const esClient = context.core.elasticsearch.client.asCurrentUser;
@ -389,9 +387,7 @@ export const langChainExecute = async ({
// Create an ElasticsearchStore for KB interactions
const kbDataClient =
(await assistantContext.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled,
})) ?? undefined;
(await assistantContext.getAIAssistantKnowledgeBaseDataClient()) ?? undefined;
const dataClients: AssistantDataClients = {
anonymizationFieldsDataClient: anonymizationFieldsDataClient ?? undefined,
@ -643,29 +639,6 @@ export const performChecks = ({
};
};
/**
* Returns whether the v2 KB is enabled
*
* @param context - Route context
* @param request - Route KibanaRequest
*/
export const isV2KnowledgeBaseEnabled = ({
context,
request,
}: {
context: AwaitedProperties<
Pick<ElasticAssistantRequestHandlerContext, 'elasticAssistant' | 'licensing' | 'core'>
>;
request: KibanaRequest;
}): boolean => {
const pluginName = getPluginNameFromRequest({
request,
defaultPluginName: DEFAULT_PLUGIN_NAME,
});
return context.elasticAssistant.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault;
};
/**
* Telemetry function to determine whether knowledge base has been installed
* @param kbDataClient
@ -674,11 +647,11 @@ export const getIsKnowledgeBaseInstalled = async (
kbDataClient?: AIAssistantKnowledgeBaseDataClient | null
): Promise<boolean> => {
let securityLabsDocsExist = false;
let isModelDeployed = false;
let isInferenceEndpointExists = false;
if (kbDataClient != null) {
try {
isModelDeployed = await kbDataClient.isModelDeployed();
if (isModelDeployed) {
isInferenceEndpointExists = await kbDataClient.isInferenceEndpointExists();
if (isInferenceEndpointExists) {
securityLabsDocsExist =
(
await kbDataClient.getKnowledgeBaseDocumentEntries({
@ -692,5 +665,5 @@ export const getIsKnowledgeBaseInstalled = async (
}
}
return isModelDeployed && securityLabsDocsExist;
return isInferenceEndpointExists && securityLabsDocsExist;
};

View file

@ -13,7 +13,6 @@ export { postAttackDiscoveryRoute } from './attack_discovery/post/post_attack_di
export { getAttackDiscoveryRoute } from './attack_discovery/get/get_attack_discovery';
// Knowledge Base
export { deleteKnowledgeBaseRoute } from './knowledge_base/delete_knowledge_base';
export { getKnowledgeBaseIndicesRoute } from './knowledge_base/get_knowledge_base_indices';
export { getKnowledgeBaseStatusRoute } from './knowledge_base/get_knowledge_base_status';
export { postKnowledgeBaseRoute } from './knowledge_base/post_knowledge_base';

View file

@ -5,8 +5,6 @@
* 2.0.
*/
export const KNOWLEDGE_BASE_INDEX_PATTERN = '.kibana-elastic-ai-assistant-kb';
export const KNOWLEDGE_BASE_INGEST_PIPELINE = '.kibana-elastic-ai-assistant-kb-ingest-pipeline';
// Query for determining if ESQL docs have been loaded, searches for a specific doc. Intended for the ElasticsearchStore.similaritySearch()
// Note: We may want to add a tag of the resource name to the document metadata, so we can CRUD by specific resource
export const ESQL_DOCS_LOADED_QUERY =

View file

@ -1,83 +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 { IRouter, KibanaRequest } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import {
ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL,
} from '@kbn/elastic-assistant-common';
import {
DeleteKnowledgeBaseRequestParams,
DeleteKnowledgeBaseResponse,
} from '@kbn/elastic-assistant-common/impl/schemas/knowledge_base/crud_kb_route.gen';
import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/schemas/common';
import { buildResponse } from '../../lib/build_response';
import { ElasticAssistantRequestHandlerContext } from '../../types';
import { isV2KnowledgeBaseEnabled } from '../helpers';
/**
* Delete Knowledge Base index, pipeline, and resources (collection of documents)
* @param router
*/
export const deleteKnowledgeBaseRoute = (
router: IRouter<ElasticAssistantRequestHandlerContext>
) => {
router.versioned
.delete({
access: 'internal',
path: ELASTIC_AI_ASSISTANT_KNOWLEDGE_BASE_URL,
options: {
tags: ['access:elasticAssistant'],
},
})
.addVersion(
{
version: ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION,
validate: {
request: {
params: buildRouteValidationWithZod(DeleteKnowledgeBaseRequestParams),
},
},
},
async (context, request: KibanaRequest<DeleteKnowledgeBaseRequestParams>, response) => {
const resp = buildResponse(response);
const ctx = await context.resolve(['core', 'elasticAssistant', 'licensing']);
const assistantContext = ctx.elasticAssistant;
const logger = ctx.elasticAssistant.logger;
// FF Check for V2 KB
const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request });
try {
const knowledgeBaseDataClient =
await assistantContext.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled,
});
if (!knowledgeBaseDataClient) {
return response.custom({ body: { success: false }, statusCode: 500 });
}
// TODO: This delete API is likely not needed and can be replaced by the new `entries` API
const body: DeleteKnowledgeBaseResponse = {
success: false,
};
return response.ok({ body });
} catch (err) {
logger.error(err);
const error = transformError(err);
return resp.error({
body: error.message,
statusCode: error.statusCode,
});
}
}
);
};

View file

@ -155,7 +155,6 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
capability: 'assistantKnowledgeBaseByDefault',
context: ctx,
request,
response,
@ -187,9 +186,7 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
// subscribing to completed$, because it handles both cases when request was completed and aborted.
// when route is finished by timeout, aborted$ is not getting fired
request.events.completed$.subscribe(() => abortController.abort());
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled: true,
});
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient();
const spaceId = ctx.elasticAssistant.getSpaceId();
const authenticatedUser = checkResponse.currentUser;
const userFilter = getKBUserFilter(authenticatedUser);
@ -288,8 +285,7 @@ export const bulkActionKnowledgeBaseEntriesRoute = (router: ElasticAssistantPlug
global: entry.users != null && entry.users.length === 0,
})
),
getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) =>
getUpdateScript({ entry, isPatch: true }),
getUpdateScript: (entry: UpdateKnowledgeBaseEntrySchema) => getUpdateScript({ entry }),
authenticatedUser,
});
const created =

View file

@ -47,7 +47,6 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
capability: 'assistantKnowledgeBaseByDefault',
context: ctx,
request,
response,
@ -56,10 +55,7 @@ export const createKnowledgeBaseEntryRoute = (router: ElasticAssistantPluginRout
return checkResponse.response;
}
// Check mappings and upgrade if necessary -- this route only supports v2 KB, so always `true`
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled: true,
});
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient();
logger.debug(() => `Creating KB Entry:\n${JSON.stringify(request.body)}`);
const createResponse = await kbDataClient?.createKnowledgeBaseEntry({

View file

@ -58,7 +58,6 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
// Perform license, authenticated user and FF checks
const checkResponse = performChecks({
capability: 'assistantKnowledgeBaseByDefault',
context: ctx,
request,
response,
@ -67,9 +66,7 @@ export const findKnowledgeBaseEntriesRoute = (router: ElasticAssistantPluginRout
return checkResponse.response;
}
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled: true,
});
const kbDataClient = await ctx.elasticAssistant.getAIAssistantKnowledgeBaseDataClient();
const currentUser = checkResponse.currentUser;
const userFilter = getKBUserFilter(currentUser);
const systemFilter = ` AND (kb_resource:"user" OR type:"index")`;

View file

@ -35,7 +35,7 @@ describe('Get Knowledge Base Status Route', () => {
},
isModelInstalled: jest.fn().mockResolvedValue(true),
isSetupAvailable: jest.fn().mockResolvedValue(true),
isModelDeployed: jest.fn().mockResolvedValue(true),
isInferenceEndpointExists: jest.fn().mockResolvedValue(true),
isSetupInProgress: false,
isSecurityLabsDocsLoaded: jest.fn().mockResolvedValue(true),
isUserDataExists: jest.fn().mockResolvedValue(true),

View file

@ -17,7 +17,6 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/
import { KibanaRequest } from '@kbn/core/server';
import { buildResponse } from '../../lib/build_response';
import { ElasticAssistantPluginRouter } from '../../types';
import { isV2KnowledgeBaseEnabled } from '../helpers';
/**
* Get the status of the Knowledge Base index, pipeline, and resources (collection of documents)
@ -49,12 +48,7 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter
const logger = ctx.elasticAssistant.logger;
try {
// FF Check for V2 KB
const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request });
const kbDataClient = await assistantContext.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled,
});
const kbDataClient = await assistantContext.getAIAssistantKnowledgeBaseDataClient();
if (!kbDataClient) {
return response.custom({ body: { success: false }, statusCode: 500 });
}
@ -63,7 +57,7 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter
const pipelineExists = true; // Installed at startup, always true
const modelExists = await kbDataClient.isModelInstalled();
const setupAvailable = await kbDataClient.isSetupAvailable();
const isModelDeployed = await kbDataClient.isModelDeployed();
const isInferenceEndpointExists = await kbDataClient.isInferenceEndpointExists();
const body: ReadKnowledgeBaseResponse = {
elser_exists: modelExists,
@ -73,13 +67,9 @@ export const getKnowledgeBaseStatusRoute = (router: ElasticAssistantPluginRouter
pipeline_exists: pipelineExists,
};
if (indexExists && isModelDeployed) {
const securityLabsExists = v2KnowledgeBaseEnabled
? await kbDataClient.isSecurityLabsDocsLoaded()
: true;
const userDataExists = v2KnowledgeBaseEnabled
? await kbDataClient.isUserDataExists()
: true;
if (indexExists && isInferenceEndpointExists) {
const securityLabsExists = await kbDataClient.isSecurityLabsDocsLoaded();
const userDataExists = await kbDataClient.isUserDataExists();
return response.ok({
body: {

View file

@ -16,7 +16,6 @@ import { buildRouteValidationWithZod } from '@kbn/elastic-assistant-common/impl/
import { IKibanaResponse } from '@kbn/core/server';
import { buildResponse } from '../../lib/build_response';
import { ElasticAssistantPluginRouter } from '../../types';
import { isV2KnowledgeBaseEnabled } from '../helpers';
// Since we're awaiting on ELSER setup, this could take a bit (especially if ML needs to autoscale)
// Consider just returning if attempt was successful, and switch to client polling
@ -54,19 +53,12 @@ export const postKnowledgeBaseRoute = (router: ElasticAssistantPluginRouter) =>
const assistantContext = ctx.elasticAssistant;
const core = ctx.core;
const soClient = core.savedObjects.getClient();
// FF Check for V2 KB
const v2KnowledgeBaseEnabled = isV2KnowledgeBaseEnabled({ context: ctx, request });
// Only allow modelId override if FF is enabled as this will re-write the ingest pipeline and break any previous KB entries
// This is only really needed for API integration tests
const modelIdOverride = v2KnowledgeBaseEnabled ? request.query.modelId : undefined;
const ignoreSecurityLabs = request.query.ignoreSecurityLabs;
try {
const knowledgeBaseDataClient =
await assistantContext.getAIAssistantKnowledgeBaseDataClient({
modelIdOverride,
v2KnowledgeBaseEnabled,
modelIdOverride: request.query.modelId,
});
if (!knowledgeBaseDataClient) {
return response.custom({ body: { success: false }, statusCode: 500 });
@ -74,7 +66,6 @@ export const postKnowledgeBaseRoute = (router: ElasticAssistantPluginRouter) =>
await knowledgeBaseDataClient.setupKnowledgeBase({
soClient,
v2KnowledgeBaseEnabled,
ignoreSecurityLabs,
});

View file

@ -23,9 +23,7 @@ import { buildResponse } from '../lib/build_response';
import { ElasticAssistantRequestHandlerContext, GetElser } from '../types';
import {
appendAssistantMessageToConversation,
DEFAULT_PLUGIN_NAME,
getIsKnowledgeBaseInstalled,
getPluginNameFromRequest,
getSystemPromptFromUserConversation,
langChainExecute,
performChecks,
@ -159,17 +157,9 @@ export const postActionsConnectorExecuteRoute = (
if (onLlmResponse) {
await onLlmResponse(error.message, {}, true);
}
const pluginName = getPluginNameFromRequest({
request,
defaultPluginName: DEFAULT_PLUGIN_NAME,
logger,
});
const v2KnowledgeBaseEnabled =
assistantContext.getRegisteredFeatures(pluginName).assistantKnowledgeBaseByDefault;
const kbDataClient =
(await assistantContext.getAIAssistantKnowledgeBaseDataClient({
v2KnowledgeBaseEnabled,
})) ?? undefined;
(await assistantContext.getAIAssistantKnowledgeBaseDataClient()) ?? undefined;
const isKnowledgeBaseInstalled = await getIsKnowledgeBaseInstalled(kbDataClient);
telemetry.reportEvent(INVOKE_ASSISTANT_ERROR_EVENT.eventType, {
actionTypeId: request.body.actionTypeId,

View file

@ -83,33 +83,28 @@ export class RequestContextFactory implements IRequestContextFactory {
telemetry: core.analytics,
// Note: Due to plugin lifecycle and feature flag registration timing, we need to pass in the feature flag here
// Remove `v2KnowledgeBaseEnabled` once 'assistantKnowledgeBaseByDefault' feature flag is removed
// Additionally, modelIdOverride is used here to enable setting up the KB using a different ELSER model, which
// Note: modelIdOverride is used here to enable setting up the KB using a different ELSER model, which
// is necessary for testing purposes (`pt_tiny_elser`).
getAIAssistantKnowledgeBaseDataClient: memoize(
async ({ modelIdOverride, v2KnowledgeBaseEnabled = false }) => {
const currentUser = getCurrentUser();
getAIAssistantKnowledgeBaseDataClient: memoize(async (params) => {
const currentUser = getCurrentUser();
const { securitySolutionAssistant } = await coreStart.capabilities.resolveCapabilities(
request,
{
capabilityPath: 'securitySolutionAssistant.*',
}
);
const { securitySolutionAssistant } = await coreStart.capabilities.resolveCapabilities(
request,
{
capabilityPath: 'securitySolutionAssistant.*',
}
);
return this.assistantService.createAIAssistantKnowledgeBaseDataClient({
spaceId: getSpaceId(),
logger: this.logger,
licensing: context.licensing,
currentUser,
modelIdOverride,
v2KnowledgeBaseEnabled,
manageGlobalKnowledgeBaseAIAssistant:
securitySolutionAssistant.manageGlobalKnowledgeBaseAIAssistant as boolean,
});
}
),
return this.assistantService.createAIAssistantKnowledgeBaseDataClient({
spaceId: getSpaceId(),
logger: this.logger,
licensing: context.licensing,
currentUser,
modelIdOverride: params?.modelIdOverride,
manageGlobalKnowledgeBaseAIAssistant:
securitySolutionAssistant.manageGlobalKnowledgeBaseAIAssistant as boolean,
});
}),
getAttackDiscoveryDataClient: memoize(() => {
const currentUser = getCurrentUser();

View file

@ -126,7 +126,7 @@ export interface ElasticAssistantApiRequestHandlerContext {
getCurrentUser: () => AuthenticatedUser | null;
getAIAssistantConversationsDataClient: () => Promise<AIAssistantConversationsDataClient | null>;
getAIAssistantKnowledgeBaseDataClient: (
params: GetAIAssistantKnowledgeBaseDataClientParams
params?: GetAIAssistantKnowledgeBaseDataClientParams
) => Promise<AIAssistantKnowledgeBaseDataClient | null>;
getAttackDiscoveryDataClient: () => Promise<AttackDiscoveryDataClient | null>;
getAIAssistantPromptsDataClient: () => Promise<AIAssistantDataClient | null>;

View file

@ -108,11 +108,6 @@ export const allowedExperimentalValues = Object.freeze({
*/
assistantModelEvaluation: false,
/**
* Enables new Knowledge Base Entries features, introduced in `8.15.0`.
*/
assistantKnowledgeBaseByDefault: true,
/**
* Enables the Managed User section inside the new user details flyout.
*/

View file

@ -1,22 +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 { getAssistantTools } from '.';
describe('getAssistantTools', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return an array of applicable tools', () => {
const tools = getAssistantTools({});
const minExpectedTools = 3; // 3 tools are currently implemented
expect(tools.length).toBeGreaterThanOrEqual(minExpectedTools);
});
});

View file

@ -14,22 +14,11 @@ import { KNOWLEDGE_BASE_RETRIEVAL_TOOL } from './knowledge_base/knowledge_base_r
import { KNOWLEDGE_BASE_WRITE_TOOL } from './knowledge_base/knowledge_base_write_tool';
import { SECURITY_LABS_KNOWLEDGE_BASE_TOOL } from './security_labs/security_labs_tool';
export const getAssistantTools = ({
assistantKnowledgeBaseByDefault,
}: {
assistantKnowledgeBaseByDefault?: boolean;
}): AssistantTool[] => {
const tools = [
ALERT_COUNTS_TOOL,
NL_TO_ESQL_TOOL,
KNOWLEDGE_BASE_RETRIEVAL_TOOL,
KNOWLEDGE_BASE_WRITE_TOOL,
OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL,
];
if (assistantKnowledgeBaseByDefault) {
tools.push(SECURITY_LABS_KNOWLEDGE_BASE_TOOL);
}
return tools;
};
export const assistantTools: AssistantTool[] = [
ALERT_COUNTS_TOOL,
NL_TO_ESQL_TOOL,
KNOWLEDGE_BASE_RETRIEVAL_TOOL,
KNOWLEDGE_BASE_WRITE_TOOL,
OPEN_AND_ACKNOWLEDGED_ALERTS_TOOL,
SECURITY_LABS_KNOWLEDGE_BASE_TOOL,
];

View file

@ -11,7 +11,6 @@ import type { AssistantTool, AssistantToolParams } from '@kbn/elastic-assistant-
import type { AIAssistantKnowledgeBaseDataClient } from '@kbn/elastic-assistant-plugin/server/ai_assistant_data_clients/knowledge_base';
import { DocumentEntryType } from '@kbn/elastic-assistant-common';
import type { KnowledgeBaseEntryCreateProps } from '@kbn/elastic-assistant-common';
import type { LegacyKnowledgeBaseEntryCreateProps } from '@kbn/elastic-assistant-plugin/server/ai_assistant_data_clients/knowledge_base/create_knowledge_base_entry';
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
import { APP_UI_ID } from '../../../../common';
@ -59,24 +58,14 @@ export const KNOWLEDGE_BASE_WRITE_TOOL: AssistantTool = {
() => `KnowledgeBaseWriteToolParams:input\n ${JSON.stringify(input, null, 2)}`
);
// Backwards compatibility with v1 schema since this feature is technically supported in `8.15`
const knowledgeBaseEntry:
| KnowledgeBaseEntryCreateProps
| LegacyKnowledgeBaseEntryCreateProps = kbDataClient.isV2KnowledgeBaseEnabled
? {
name: input.name,
kbResource: 'user',
source: 'conversation',
required: input.required,
text: input.query,
type: DocumentEntryType.value,
}
: {
type: DocumentEntryType.value,
name: 'unknown',
metadata: { kbResource: 'user', source: 'conversation', required: input.required },
text: input.query,
};
const knowledgeBaseEntry: KnowledgeBaseEntryCreateProps = {
name: input.name,
kbResource: 'user',
source: 'conversation',
required: input.required,
text: input.query,
type: DocumentEntryType.value,
};
logger.debug(() => `knowledgeBaseEntry\n ${JSON.stringify(knowledgeBaseEntry, null, 2)}`);
const resp = await kbDataClient.createKnowledgeBaseEntry({ knowledgeBaseEntry, telemetry });

View file

@ -120,7 +120,7 @@ import {
allRiskScoreIndexPattern,
} from '../common/entity_analytics/risk_engine';
import { isEndpointPackageV2 } from '../common/endpoint/utils/package_v2';
import { getAssistantTools } from './assistant/tools';
import { assistantTools } from './assistant/tools';
import { turnOffAgentPolicyFeatures } from './endpoint/migrations/turn_off_agent_policy_features';
import { getCriblPackagePolicyPostCreateOrUpdateCallback } from './security_integrations';
import { scheduleEntityAnalyticsMigration } from './lib/entity_analytics/migrations';
@ -554,15 +554,8 @@ export class Plugin implements ISecuritySolutionPlugin {
this.licensing$ = plugins.licensing.license$;
// Assistant Tool and Feature Registration
plugins.elasticAssistant.registerTools(
APP_UI_ID,
getAssistantTools({
assistantKnowledgeBaseByDefault:
config.experimentalFeatures.assistantKnowledgeBaseByDefault,
})
);
plugins.elasticAssistant.registerTools(APP_UI_ID, assistantTools);
const features = {
assistantKnowledgeBaseByDefault: config.experimentalFeatures.assistantKnowledgeBaseByDefault,
assistantModelEvaluation: config.experimentalFeatures.assistantModelEvaluation,
};
plugins.elasticAssistant.registerFeatures(APP_UI_ID, features);

View file

@ -16200,7 +16200,6 @@
"xpack.elasticAssistant.evaluation.fetchEvaluationDataError": "Erreur lors de la récupération des données d'évaluation…",
"xpack.elasticAssistant.flyout.right.header.collapseDetailButtonAriaLabel": "Masquer les chats",
"xpack.elasticAssistant.flyout.right.header.expandDetailButtonAriaLabel": "Afficher les chats",
"xpack.elasticAssistant.knowledgeBase.deleteError": "Erreur lors de la suppression de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.entries.createErrorTitle": "Erreur lors de la création dune entrée de la base de connaissances",
"xpack.elasticAssistant.knowledgeBase.entries.createSuccessTitle": "Entrée de la base de connaissances créée",
"xpack.elasticAssistant.knowledgeBase.entries.deleteErrorTitle": "Erreur lors de la suppression dentrées de la base de connaissances",

View file

@ -16176,7 +16176,6 @@
"xpack.elasticAssistant.evaluation.fetchEvaluationDataError": "評価データの取得エラー...",
"xpack.elasticAssistant.flyout.right.header.collapseDetailButtonAriaLabel": "チャットを非表示",
"xpack.elasticAssistant.flyout.right.header.expandDetailButtonAriaLabel": "チャットを表示",
"xpack.elasticAssistant.knowledgeBase.deleteError": "ナレッジベースの削除エラー",
"xpack.elasticAssistant.knowledgeBase.entries.createErrorTitle": "ナレッジベースエントリの作成エラー",
"xpack.elasticAssistant.knowledgeBase.entries.createSuccessTitle": "ナレッジベースエントリが作成されました",
"xpack.elasticAssistant.knowledgeBase.entries.deleteErrorTitle": "ナレッジベースエントリの削除エラー",

View file

@ -15859,7 +15859,6 @@
"xpack.elasticAssistant.evaluation.fetchEvaluationDataError": "提取评估数据时出错......",
"xpack.elasticAssistant.flyout.right.header.collapseDetailButtonAriaLabel": "隐藏聊天",
"xpack.elasticAssistant.flyout.right.header.expandDetailButtonAriaLabel": "显示聊天",
"xpack.elasticAssistant.knowledgeBase.deleteError": "删除知识库时出错",
"xpack.elasticAssistant.knowledgeBase.entries.createErrorTitle": "创建知识库条目时出错",
"xpack.elasticAssistant.knowledgeBase.entries.createSuccessTitle": "已创建知识库条目",
"xpack.elasticAssistant.knowledgeBase.entries.deleteErrorTitle": "删除知识库条目时出错",

View file

@ -30,9 +30,6 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi
'--xpack.ruleRegistry.write.enabled=true',
'--xpack.ruleRegistry.write.enabled=true',
'--xpack.ruleRegistry.write.cache.enabled=false',
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'assistantKnowledgeBaseByDefault',
])}`,
'--monitoring_collection.opentelemetry.metrics.prometheus.enabled=true',
],
},

View file

@ -28,9 +28,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
)
),
'--elasticsearch.hosts=http://localhost:9220',
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'assistantKnowledgeBaseByDefault',
])}`,
],
},
testFiles: [require.resolve('..')],

View file

@ -9,9 +9,6 @@ import { createTestConfig } from '../../../../../../config/serverless/config.bas
export default createTestConfig({
kbnTestServerArgs: [
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
'assistantKnowledgeBaseByDefault',
])}`,
`--xpack.securitySolutionServerless.productTypes=${JSON.stringify([
{ product_line: 'security', product_tier: 'complete' },
{ product_line: 'endpoint', product_tier: 'complete' },

View file

@ -39,9 +39,3 @@ export const indexEntry: IndexEntryCreateFields = {
queryDescription: 'Use sample-field to search in sample-index',
users: undefined,
};
export const globalIndexEntry: IndexEntryCreateFields = {
...indexEntry,
name: 'Sample Global Index Entry',
users: [],
};