diff --git a/x-pack/platform/plugins/shared/streams/server/routes/utils/resolve_connector_for_feature.ts b/x-pack/platform/plugins/shared/streams/server/routes/utils/resolve_connector_for_feature.ts index 68a5c6b0b188c..ec9f68b7831df 100644 --- a/x-pack/platform/plugins/shared/streams/server/routes/utils/resolve_connector_for_feature.ts +++ b/x-pack/platform/plugins/shared/streams/server/routes/utils/resolve_connector_for_feature.ts @@ -15,8 +15,10 @@ import { StatusError } from '../../lib/streams/errors/status_error'; * * Resolution order (delegated to `getForFeature`): * 1. Admin override in the `inference_settings` SO (set via Model Settings page) - * 2. `recommendedEndpoints` from the feature registration - * 3. Platform default connector + * 2. Kibana global default (`GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR`), or the + * strict-default short-circuit when `GEN_AI_SETTINGS_DEFAULT_AI_CONNECTOR_DEFAULT_ONLY=true` + * 3. `recommendedEndpoints` from the feature registration + * 4. Platform default connector * * @throws StatusError(503) if the searchInferenceEndpoints plugin is unavailable. * @throws StatusError(400) if no connector resolves for the feature. diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx index c6b65e323dda4..e4c69d358700d 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/ki_generation_context.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import type { InferenceConnector } from '@kbn/inference-common'; import type { ListStreamDetail } from '@kbn/streams-plugin/server/routes/internal/streams/crud/route'; import type { OnboardingResult, TaskResult } from '@kbn/streams-schema'; import { @@ -32,8 +33,10 @@ import type { OnboardingConfig } from '../shared/types'; const IN_PROGRESS_STATUSES = new Set([TaskStatus.InProgress, TaskStatus.BeingCanceled]); interface ConnectorState { + connectors: InferenceConnector[]; resolvedConnectorId: string | undefined; loading: boolean; + error: Error | undefined; } interface KiGenerationContextValue { diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx index d215aff32bb4e..54278c85c4062 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/knowledge_indicators_table/knowledge_indicators_table.tsx @@ -32,7 +32,6 @@ import { useQueriesApi, type PromoteResult } from '../../../../../hooks/sig_even import { useInvalidatePromoteRelatedQueries } from '../../../../../hooks/sig_events/use_invalidate_promote_queries'; import { getFormattedError } from '../../../../../util/errors'; import { useKibana } from '../../../../../hooks/use_kibana'; -import { useAIFeatures } from '../../../../../hooks/use_ai_features'; import { AssetImage } from '../../../../asset_image'; import { LoadingPanel } from '../../../../loading_panel'; import { KnowledgeIndicatorDetailsFlyout } from '../../../stream_detail_significant_events_view/knowledge_indicator_details_flyout'; @@ -107,11 +106,11 @@ export function KnowledgeIndicatorsTable() { bulkOnboardQueriesOnly, } = useKiGeneration(); - const aiFeatures = useAIFeatures(); - const allConnectors = aiFeatures?.genAiConnectors?.connectors ?? []; - const connectorError = aiFeatures?.genAiConnectors?.error; - const isConnectorCatalogUnavailable = - !allConnectors.length || !!aiFeatures?.genAiConnectors?.loading || !!connectorError; + const connectorError = featuresConnectors.error ?? queriesConnectors.error; + const hasAnyConnectors = + featuresConnectors.connectors.length > 0 || queriesConnectors.connectors.length > 0; + const isAnyLoading = featuresConnectors.loading || queriesConnectors.loading; + const isConnectorCatalogUnavailable = !hasAnyConnectors || isAnyLoading || !!connectorError; const runAndClearPicker = useCallback( async (action: (names: string[]) => Promise) => { @@ -220,7 +219,8 @@ export function KnowledgeIndicatorsTable() { { const featuresConnector = useMemo( - () => allConnectors.find((c) => c.connectorId === config.connectors.features), - [allConnectors, config.connectors.features] + () => featuresConnectors.find((c) => c.connectorId === config.connectors.features), + [featuresConnectors, config.connectors.features] ); const queriesConnector = useMemo( - () => allConnectors.find((c) => c.connectorId === config.connectors.queries), - [allConnectors, config.connectors.queries] + () => queriesConnectors.find((c) => c.connectorId === config.connectors.queries), + [queriesConnectors, config.connectors.queries] ); const onSelectFeaturesConnector = useCallback( @@ -109,7 +120,7 @@ export const GenerateSplitButton = ({ ], }, buildConnectorSelectionPanel({ - connectors: allConnectors, + connectors: featuresConnectors, resolvedConnectorId: featuresResolvedConnectorId, selectedConnectorId: config.connectors.features, onSelect: (connectorId) => { @@ -118,7 +129,7 @@ export const GenerateSplitButton = ({ }, }), buildConnectorSelectionPanel({ - connectors: allConnectors, + connectors: queriesConnectors, resolvedConnectorId: queriesResolvedConnectorId, selectedConnectorId: config.connectors.queries, onSelect: (connectorId) => { @@ -131,7 +142,8 @@ export const GenerateSplitButton = ({ isRunDisabled, featuresConnector, queriesConnector, - allConnectors, + featuresConnectors, + queriesConnectors, featuresResolvedConnectorId, queriesResolvedConnectorId, config.connectors.features, diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx index f634162c7e85e..983499ab204d4 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/insights_split_button.tsx @@ -20,7 +20,11 @@ import { ContextMenuSplitButton } from '../shared/context_menu_split_button'; import type { MenuHelpers } from '../shared/context_menu_split_button'; interface InsightsSplitButtonProps { - allConnectors: InferenceConnector[]; + /** + * Connectors scoped to the Discovery feature: recommended endpoints first, + * then the rest of the catalog. Drives the dropdown options. + */ + discoveryConnectors: InferenceConnector[]; connectorError: Error | undefined; resolvedConnectorId: string | undefined; displayConnectorId: string | undefined; @@ -31,7 +35,7 @@ interface InsightsSplitButtonProps { } export const InsightsSplitButton = ({ - allConnectors, + discoveryConnectors, connectorError, resolvedConnectorId, displayConnectorId, @@ -41,8 +45,8 @@ export const InsightsSplitButton = ({ isDisabled, }: InsightsSplitButtonProps) => { const discoveryConnector = useMemo( - () => allConnectors.find((c) => c.connectorId === displayConnectorId), - [allConnectors, displayConnectorId] + () => discoveryConnectors.find((c) => c.connectorId === displayConnectorId), + [discoveryConnectors, displayConnectorId] ); const buildPanels = useCallback( @@ -51,7 +55,7 @@ export const InsightsSplitButton = ({ items: [buildConnectorMenuItem({ connector: discoveryConnector, panelId: 1 })], }, buildConnectorSelectionPanel({ - connectors: allConnectors, + connectors: discoveryConnectors, resolvedConnectorId, selectedConnectorId: displayConnectorId, onSelect: (connectorId) => { @@ -60,7 +64,13 @@ export const InsightsSplitButton = ({ }, }), ], - [discoveryConnector, allConnectors, resolvedConnectorId, displayConnectorId, onConnectorChange] + [ + discoveryConnector, + discoveryConnectors, + resolvedConnectorId, + displayConnectorId, + onConnectorChange, + ] ); return ( diff --git a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx index 303e63d02b3ae..a4f212df2f359 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx +++ b/x-pack/platform/plugins/shared/streams_app/public/components/sig_events/significant_events_discovery/components/streams_view/streams_view.tsx @@ -15,7 +15,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import type { TableRow } from './utils'; import { useInferenceFeatureConnectors } from '../../../../../hooks/sig_events/use_inference_feature_connectors'; -import { useAIFeatures } from '../../../../../hooks/use_ai_features'; import { useKibana } from '../../../../../hooks/use_kibana'; import { useInsightsDiscoveryApi } from '../../../../../hooks/sig_events/use_insights_discovery_api'; import { useStreamsAppRouter } from '../../../../../hooks/use_streams_app_router'; @@ -74,11 +73,16 @@ export function StreamsView() { const discoveryConnectors = useInferenceFeatureConnectors( STREAMS_SIG_EVENTS_DISCOVERY_INFERENCE_FEATURE_ID ); - const aiFeatures = useAIFeatures(); - const allConnectors = aiFeatures?.genAiConnectors?.connectors ?? []; - const connectorError = aiFeatures?.genAiConnectors?.error; - const isConnectorCatalogUnavailable = - !allConnectors.length || !!aiFeatures?.genAiConnectors?.loading || !!connectorError; + + const connectorError = + featuresConnectors.error ?? queriesConnectors.error ?? discoveryConnectors.error; + const hasAnyConnectors = + featuresConnectors.connectors.length > 0 || + queriesConnectors.connectors.length > 0 || + discoveryConnectors.connectors.length > 0; + const isAnyLoading = + featuresConnectors.loading || queriesConnectors.loading || discoveryConnectors.loading; + const isConnectorCatalogUnavailable = !hasAnyConnectors || isAnyLoading || !!connectorError; const [discoveryConnectorOverride, setDiscoveryConnectorOverride] = useState< string | undefined @@ -233,7 +237,8 @@ export function StreamsView() { global default > feature recommendeds > + * rest of catalog), so we render its response verbatim and pick the first + * entry as the pre-selected model. */ export function useInferenceFeatureConnectors( featureId: string @@ -30,17 +48,16 @@ export function useInferenceFeatureConnectors( } = useKibana(); const query = useLoadConnectors({ http, toasts, featureId }); - const connectors = query.data ?? []; + const aiConnectors = query.data; - // When an SO entry exists the API puts the configured connector first. - // Otherwise the API prepends the global default before the recommended - // ones, so we skip it and pick the first recommended connector instead. - const picked = query.soEntryFound - ? connectors[0] - : connectors.find((c) => c.isRecommended) ?? connectors[0]; + const connectors = useMemo( + () => (aiConnectors?.length ? aiConnectors.map(toInferenceConnector) : EMPTY_CONNECTORS), + [aiConnectors] + ); return { - resolvedConnectorId: picked?.id, + connectors, + resolvedConnectorId: aiConnectors?.[0]?.id, loading: query.isLoading || query.isFetching, error: query.error ?? undefined, }; diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/to_inference_connector.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/to_inference_connector.ts new file mode 100644 index 0000000000000..bc50430e3858e --- /dev/null +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/to_inference_connector.ts @@ -0,0 +1,28 @@ +/* + * 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 { InferenceConnector, InferenceConnectorType } from '@kbn/inference-common'; +import type { AIConnector } from '@kbn/inference-connectors'; + +/** + * Adapts an {@link AIConnector} (returned by `@kbn/inference-connectors`'s + * `useLoadConnectors`) to the {@link InferenceConnector} shape consumed by + * downstream Streams components. Kept in one place so both the GenAI parent + * hook and the per-feature sig-events hook agree on the mapping. + */ +export const toInferenceConnector = (c: AIConnector): InferenceConnector => ({ + connectorId: c.id, + name: c.name, + type: c.actionTypeId as InferenceConnectorType, + config: 'config' in c ? (c.config as Record) : {}, + capabilities: {}, + isPreconfigured: c.isPreconfigured, + isInferenceEndpoint: false, + isEis: c.isEis, + isDeprecated: c.isDeprecated, + isMissingSecrets: c.isMissingSecrets, +}); diff --git a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_genai_connectors.ts b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_genai_connectors.ts index 5d3c59f9560d7..8f10432f67c61 100644 --- a/x-pack/platform/plugins/shared/streams_app/public/hooks/use_genai_connectors.ts +++ b/x-pack/platform/plugins/shared/streams_app/public/hooks/use_genai_connectors.ts @@ -9,9 +9,10 @@ import { useCallback, useMemo } from 'react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { SettingsStart } from '@kbn/core-ui-settings-browser'; -import type { InferenceConnector, InferenceConnectorType } from '@kbn/inference-common'; -import { useLoadConnectors, type AIConnector } from '@kbn/inference-connectors'; +import type { InferenceConnector } from '@kbn/inference-common'; +import { useLoadConnectors } from '@kbn/inference-connectors'; import { STREAMS_INFERENCE_PARENT_FEATURE_ID } from '@kbn/streams-schema'; +import { toInferenceConnector } from './to_inference_connector'; const STREAMS_CONNECTOR_STORAGE_KEY = 'xpack.streamsApp.lastUsedConnector'; const OLD_STORAGE_KEY = 'xpack.observabilityAiAssistant.lastUsedConnector'; @@ -26,19 +27,6 @@ export interface UseGenAIConnectorsResult { isConnectorSelectionRestricted: boolean; } -const toInferenceConnector = (c: AIConnector): InferenceConnector => ({ - connectorId: c.id, - name: c.name, - type: c.actionTypeId as InferenceConnectorType, - config: 'config' in c ? (c.config as Record) : {}, - capabilities: {}, - isPreconfigured: c.isPreconfigured, - isInferenceEndpoint: false, - isEis: c.isEis, - isDeprecated: c.isDeprecated, - isMissingSecrets: c.isMissingSecrets, -}); - export function useGenAIConnectors({ http, settings,