mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security solution] assistantKnowledgeBaseByDefault
flag removed (#198180)
This commit is contained in:
parent
0ab841f6d6
commit
194de0dadb
84 changed files with 254 additions and 3169 deletions
|
@ -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 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -20,12 +20,9 @@ paths:
|
|||
schema:
|
||||
type: object
|
||||
properties:
|
||||
assistantKnowledgeBaseByDefault:
|
||||
type: boolean
|
||||
assistantModelEvaluation:
|
||||
type: boolean
|
||||
required:
|
||||
- assistantKnowledgeBaseByDefault
|
||||
- assistantModelEvaluation
|
||||
'400':
|
||||
description: Generic Error
|
||||
|
|
|
@ -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({
|
||||
/**
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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';
|
|
@ -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(() => {
|
||||
|
|
|
@ -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', () => ({
|
||||
|
|
|
@ -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} />, {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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([
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -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
|
@ -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',
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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!',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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 ?? '',
|
||||
},
|
||||
})) ?? [];
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
|
@ -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 } },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
},
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
});
|
|
@ -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[];
|
||||
};
|
||||
}
|
|
@ -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([]);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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 =
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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")`;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 d’une 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 d’entrées de la base de connaissances",
|
||||
|
|
|
@ -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": "ナレッジベースエントリの削除エラー",
|
||||
|
|
|
@ -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": "删除知识库条目时出错",
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
|
|
|
@ -28,9 +28,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
)
|
||||
),
|
||||
'--elasticsearch.hosts=http://localhost:9220',
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'assistantKnowledgeBaseByDefault',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
testFiles: [require.resolve('..')],
|
||||
|
|
|
@ -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' },
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue