[Inference Connector] Modified getProvider to use _inference/_services ES API instead of hardcoded values. (#199047)

@ymao1
[introduced](https://github.com/elastic/elasticsearch/pull/114862) new
ES API which allows to fetch available services providers list with the
settings and task types:
`GET _inference/_services` 
This PR is changing hardcoded providers list
`x-pack/plugins/stack_connectors/public/connector_types/inference/providers/get_providers.ts`
to use new ES API.
This commit is contained in:
Yuliia Naumenko 2024-11-11 17:50:29 -08:00 committed by GitHub
parent 4e65ae9b1e
commit abf6a1d4f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 301 additions and 1761 deletions

View file

@ -19,6 +19,7 @@ import {
TextEmbeddingParamsSchema,
TextEmbeddingResponseSchema,
} from './schema';
import { ConfigProperties } from '../dynamic_config/types';
export type Config = TypeOf<typeof ConfigSchema>;
export type Secrets = TypeOf<typeof SecretsSchema>;
@ -36,3 +37,17 @@ export type TextEmbeddingParams = TypeOf<typeof TextEmbeddingParamsSchema>;
export type TextEmbeddingResponse = TypeOf<typeof TextEmbeddingResponseSchema>;
export type StreamingResponse = TypeOf<typeof StreamingResponseSchema>;
export type FieldsConfiguration = Record<string, ConfigProperties>;
export interface InferenceTaskType {
task_type: string;
configuration: FieldsConfiguration;
}
export interface InferenceProvider {
provider: string;
task_types: InferenceTaskType[];
logo?: string;
configuration: FieldsConfiguration;
}

View file

@ -32,10 +32,10 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import { ConfigEntryView } from '../../../common/dynamic_config/types';
import { ConnectorConfigurationFormItems } from '../lib/dynamic_config/connector_configuration_form_items';
import * as i18n from './translations';
import { DEFAULT_TASK_TYPE } from './constants';
import { ConfigEntryView } from '../lib/dynamic_config/types';
import { Config } from './types';
import { TaskTypeOption } from './helpers';
@ -52,7 +52,7 @@ interface AdditionalOptionsConnectorFieldsProps {
isEdit: boolean;
optionalProviderFormFields: ConfigEntryView[];
onSetProviderConfigEntry: (key: string, value: unknown) => Promise<void>;
onTaskTypeOptionsSelect: (taskType: string, provider?: string) => Promise<void>;
onTaskTypeOptionsSelect: (taskType: string, provider?: string) => void;
selectedTaskType?: string;
taskTypeFormFields: ConfigEntryView[];
taskTypeSchema: ConfigEntryView[];

View file

@ -12,13 +12,10 @@ import { ConnectorFormTestProvider } from '../lib/test_utils';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createStartServicesMock } from '@kbn/triggers-actions-ui-plugin/public/common/lib/kibana/kibana_react.mock';
import { DisplayType, FieldType } from '../lib/dynamic_config/types';
import { useProviders } from './providers/get_providers';
import { getTaskTypes } from './get_task_types';
import { HttpSetup } from '@kbn/core-http-browser';
import { DisplayType, FieldType } from '../../../common/dynamic_config/types';
jest.mock('./providers/get_providers');
jest.mock('./get_task_types');
const mockUseKibanaReturnValue = createStartServicesMock();
jest.mock('@kbn/triggers-actions-ui-plugin/public/common/lib/kibana', () => ({
@ -37,13 +34,32 @@ jest.mock('@faker-js/faker', () => ({
}));
const mockProviders = useProviders as jest.Mock;
const mockTaskTypes = getTaskTypes as jest.Mock;
const providersSchemas = [
{
provider: 'openai',
logo: '', // should be openai logo here, the hardcoded uses assets/images
taskTypes: ['completion', 'text_embedding'],
task_types: [
{
task_type: 'completion',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
configuration: {
api_key: {
display: DisplayType.TEXTBOX,
@ -106,7 +122,16 @@ const providersSchemas = [
{
provider: 'googleaistudio',
logo: '', // should be googleaistudio logo here, the hardcoded uses assets/images
taskTypes: ['completion', 'text_embedding'],
task_types: [
{
task_type: 'completion',
configuration: {},
},
{
task_type: 'text_embedding',
configuration: {},
},
],
configuration: {
api_key: {
display: DisplayType.TEXTBOX,
@ -139,39 +164,6 @@ const providersSchemas = [
},
},
];
const taskTypesSchemas: Record<string, any> = {
googleaistudio: [
{
task_type: 'completion',
configuration: {},
},
{
task_type: 'text_embedding',
configuration: {},
},
],
openai: [
{
task_type: 'completion',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
};
const openAiConnector = {
actionTypeId: '.inference',
@ -222,9 +214,6 @@ describe('ConnectorFields renders', () => {
isLoading: false,
data: providersSchemas,
});
mockTaskTypes.mockImplementation(
(http: HttpSetup, provider: string) => taskTypesSchemas[provider]
);
});
test('openai provider fields are rendered', async () => {
const { getAllByTestId } = render(

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
EuiFormRow,
EuiSpacer,
@ -31,12 +31,12 @@ import {
import { useKibana } from '@kbn/triggers-actions-ui-plugin/public';
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
import { ConfigEntryView } from '../../../common/dynamic_config/types';
import { InferenceTaskType } from '../../../common/inference/types';
import { ServiceProviderKeys } from '../../../common/inference/constants';
import { ConnectorConfigurationFormItems } from '../lib/dynamic_config/connector_configuration_form_items';
import { getTaskTypes } from './get_task_types';
import * as i18n from './translations';
import { DEFAULT_TASK_TYPE } from './constants';
import { ConfigEntryView } from '../lib/dynamic_config/types';
import { SelectableProvider } from './providers/selectable';
import { Config, Secrets } from './types';
import { generateInferenceEndpointId, getTaskTypeOptions, TaskTypeOption } from './helpers';
@ -116,13 +116,13 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
}, [isSubmitting, config, validateFields]);
const onTaskTypeOptionsSelect = useCallback(
async (taskType: string, provider?: string) => {
(taskType: string, provider?: string) => {
// Get task type settings
const currentTaskTypes = await getTaskTypes(http, provider ?? config?.provider);
const currentProvider = providers?.find((p) => p.provider === (provider ?? config?.provider));
const currentTaskTypes = currentProvider?.task_types;
const newTaskType = currentTaskTypes?.find((p) => p.task_type === taskType);
setSelectedTaskType(taskType);
generateInferenceEndpointId(config, setFieldValue);
// transform the schema
const newTaskTypeSchema = Object.keys(newTaskType?.configuration ?? {}).map((k) => ({
@ -150,19 +150,23 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
taskTypeConfig: configDefaults,
},
});
generateInferenceEndpointId(
{ ...config, taskType, taskTypeConfig: configDefaults },
setFieldValue
);
},
[config, http, setFieldValue, updateFieldValues]
[config, providers, setFieldValue, updateFieldValues]
);
const onProviderChange = useCallback(
async (provider?: string) => {
(provider?: string) => {
const newProvider = providers?.find((p) => p.provider === provider);
// Update task types list available for the selected provider
const providerTaskTypes = newProvider?.taskTypes ?? [];
const providerTaskTypes = (newProvider?.task_types ?? []).map((t) => t.task_type);
setTaskTypeOptions(getTaskTypeOptions(providerTaskTypes));
if (providerTaskTypes.length > 0) {
await onTaskTypeOptionsSelect(providerTaskTypes[0], provider);
onTaskTypeOptionsSelect(providerTaskTypes[0], provider);
}
// Update connector providerSchema
@ -203,9 +207,8 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
);
useEffect(() => {
const getTaskTypeSchema = async () => {
const currentTaskTypes = await getTaskTypes(http, config?.provider ?? '');
const newTaskType = currentTaskTypes?.find((p) => p.task_type === config?.taskType);
const getTaskTypeSchema = (taskTypes: InferenceTaskType[]) => {
const newTaskType = taskTypes.find((p) => p.task_type === config?.taskType);
// transform the schema
const newTaskTypeSchema = Object.keys(newTaskType?.configuration ?? {}).map((k) => ({
@ -228,7 +231,7 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
setProviderSchema(newProviderSchema);
getTaskTypeSchema();
getTaskTypeSchema(newProvider?.task_types ?? []);
}
}, [config?.provider, config?.taskType, http, isEdit, providers]);
@ -309,6 +312,22 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
setFieldValue('config.provider', '');
}, [onProviderChange, setFieldValue]);
const providerIcon = useMemo(
() =>
Object.keys(SERVICE_PROVIDERS).includes(config?.provider)
? SERVICE_PROVIDERS[config?.provider as ServiceProviderKeys].icon
: undefined,
[config?.provider]
);
const providerName = useMemo(
() =>
Object.keys(SERVICE_PROVIDERS).includes(config?.provider)
? SERVICE_PROVIDERS[config?.provider as ServiceProviderKeys].name
: config?.provider,
[config?.provider]
);
const providerSuperSelect = useCallback(
(isInvalid: boolean) => (
<EuiFormControlLayout
@ -317,11 +336,7 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
isDisabled={isEdit || readOnly}
isInvalid={isInvalid}
fullWidth
icon={
!config?.provider
? { type: 'sparkles', side: 'left' }
: SERVICE_PROVIDERS[config?.provider as ServiceProviderKeys].icon
}
icon={!config?.provider ? { type: 'sparkles', side: 'left' } : providerIcon}
>
<EuiFieldText
onClick={handleProviderPopover}
@ -329,9 +344,7 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
isInvalid={isInvalid}
disabled={isEdit || readOnly}
onKeyDown={handleProviderKeyboardOpen}
value={
config?.provider ? SERVICE_PROVIDERS[config?.provider as ServiceProviderKeys].name : ''
}
value={config?.provider ? providerName : ''}
fullWidth
placeholder={i18n.SELECT_PROVIDER}
icon={{ type: 'arrowDown', side: 'right' }}
@ -345,8 +358,10 @@ const InferenceAPIConnectorFields: React.FunctionComponent<ActionConnectorFields
readOnly,
onClearProvider,
config?.provider,
providerIcon,
handleProviderPopover,
handleProviderKeyboardOpen,
providerName,
isProviderPopoverOpen,
]
);

View file

@ -1,50 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { httpServiceMock } from '@kbn/core/public/mocks';
import { DisplayType, FieldType } from '../lib/dynamic_config/types';
import { getTaskTypes } from './get_task_types';
const http = httpServiceMock.createStartContract();
beforeEach(() => jest.resetAllMocks());
describe.skip('getTaskTypes', () => {
test('should call get inference task types api', async () => {
const apiResponse = {
amazonbedrock: [
{
task_type: 'completion',
configuration: {
max_new_tokens: {
display: DisplayType.NUMERIC,
label: 'Max new tokens',
order: 1,
required: false,
sensitive: false,
tooltip: 'Sets the maximum number for the output tokens to be generated.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'text_embedding',
configuration: {},
},
],
};
http.get.mockResolvedValueOnce(apiResponse);
const result = await getTaskTypes(http, 'amazonbedrock');
expect(result).toEqual(apiResponse);
});
});

View file

@ -1,606 +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 { HttpSetup } from '@kbn/core-http-browser';
import { DisplayType, FieldType } from '../lib/dynamic_config/types';
import { FieldsConfiguration } from './types';
export interface InferenceTaskType {
task_type: string;
configuration: FieldsConfiguration;
}
// this http param is for the future migrating to real API
export const getTaskTypes = (http: HttpSetup, provider: string): Promise<InferenceTaskType[]> => {
const providersTaskTypes: Record<string, InferenceTaskType[]> = {
openai: [
{
task_type: 'completion',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'text_embedding',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
mistral: [
{
task_type: 'text_embedding',
configuration: {},
},
],
hugging_face: [
{
task_type: 'text_embedding',
configuration: {},
},
],
googlevertexai: [
{
task_type: 'text_embedding',
configuration: {
auto_truncate: {
display: DisplayType.TOGGLE,
label: 'Auto truncate',
order: 1,
required: false,
sensitive: false,
tooltip:
'Specifies if the API truncates inputs longer than the maximum token length automatically.',
type: FieldType.BOOLEAN,
validations: [],
value: false,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'rerank',
configuration: {
top_n: {
display: DisplayType.TOGGLE,
label: 'Top N',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the number of the top n documents, which should be returned.',
type: FieldType.BOOLEAN,
validations: [],
value: false,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
googleaistudio: [
{
task_type: 'completion',
configuration: {},
},
{
task_type: 'text_embedding',
configuration: {},
},
],
elasticsearch: [
{
task_type: 'rerank',
configuration: {
return_documents: {
display: DisplayType.TOGGLE,
label: 'Return documents',
options: [],
order: 1,
required: false,
sensitive: false,
tooltip: 'Returns the document instead of only the index.',
type: FieldType.BOOLEAN,
validations: [],
value: true,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'sparse_embedding',
configuration: {},
},
{
task_type: 'text_embedding',
configuration: {},
},
],
cohere: [
{
task_type: 'completion',
configuration: {},
},
{
task_type: 'text_embedding',
configuration: {
input_type: {
display: DisplayType.DROPDOWN,
label: 'Input type',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the type of input passed to the model.',
type: FieldType.STRING,
validations: [],
options: [
{
label: 'classification',
value: 'classification',
},
{
label: 'clusterning',
value: 'clusterning',
},
{
label: 'ingest',
value: 'ingest',
},
{
label: 'search',
value: 'search',
},
],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
truncate: {
display: DisplayType.DROPDOWN,
options: [
{
label: 'NONE',
value: 'NONE',
},
{
label: 'START',
value: 'START',
},
{
label: 'END',
value: 'END',
},
],
label: 'Truncate',
order: 2,
required: false,
sensitive: false,
tooltip: 'Specifies how the API handles inputs longer than the maximum token length.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'rerank',
configuration: {
return_documents: {
display: DisplayType.TOGGLE,
label: 'Return documents',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specify whether to return doc text within the results.',
type: FieldType.BOOLEAN,
validations: [],
value: false,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
top_n: {
display: DisplayType.NUMERIC,
label: 'Top N',
order: 1,
required: false,
sensitive: false,
tooltip:
'The number of most relevant documents to return, defaults to the number of the documents.',
type: FieldType.INTEGER,
validations: [],
value: false,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
azureopenai: [
{
task_type: 'completion',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'text_embedding',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
azureaistudio: [
{
task_type: 'completion',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'text_embedding',
configuration: {
do_sample: {
display: DisplayType.NUMERIC,
label: 'Do sample',
order: 1,
required: false,
sensitive: false,
tooltip: 'Instructs the inference process to perform sampling or not.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
max_new_tokens: {
display: DisplayType.NUMERIC,
label: 'Max new tokens',
order: 1,
required: false,
sensitive: false,
tooltip: 'Provides a hint for the maximum number of output tokens to be generated.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
temperature: {
display: DisplayType.NUMERIC,
label: 'Temperature',
order: 1,
required: false,
sensitive: false,
tooltip: 'A number in the range of 0.0 to 2.0 that specifies the sampling temperature.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
top_p: {
display: DisplayType.NUMERIC,
label: 'Top P',
order: 1,
required: false,
sensitive: false,
tooltip:
'A number in the range of 0.0 to 2.0 that is an alternative value to temperature. Should not be used if temperature is specified.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
amazonbedrock: [
{
task_type: 'completion',
configuration: {
max_new_tokens: {
display: DisplayType.NUMERIC,
label: 'Max new tokens',
order: 1,
required: false,
sensitive: false,
tooltip: 'Sets the maximum number for the output tokens to be generated.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
temperature: {
display: DisplayType.NUMERIC,
label: 'Temperature',
order: 1,
required: false,
sensitive: false,
tooltip:
'A number between 0.0 and 1.0 that controls the apparent creativity of the results.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
top_p: {
display: DisplayType.NUMERIC,
label: 'Top P',
order: 1,
required: false,
sensitive: false,
tooltip:
'Alternative to temperature. A number in the range of 0.0 to 1.0, to eliminate low-probability tokens.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
top_k: {
display: DisplayType.NUMERIC,
label: 'Top K',
order: 1,
required: false,
sensitive: false,
tooltip:
'Only available for anthropic, cohere, and mistral providers. Alternative to temperature.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'text_embedding',
configuration: {},
},
],
anthropic: [
{
task_type: 'completion',
configuration: {
max_tokens: {
display: DisplayType.NUMERIC,
label: 'Max tokens',
order: 1,
required: true,
sensitive: false,
tooltip: 'The maximum number of tokens to generate before stopping.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
temperature: {
display: DisplayType.TEXTBOX,
label: 'Temperature',
order: 2,
required: false,
sensitive: false,
tooltip: 'The amount of randomness injected into the response.',
type: FieldType.STRING,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
top_p: {
display: DisplayType.NUMERIC,
label: 'Top P',
order: 4,
required: false,
sensitive: false,
tooltip: 'Specifies to use Anthropics nucleus sampling.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
top_k: {
display: DisplayType.NUMERIC,
label: 'Top K',
order: 3,
required: false,
sensitive: false,
tooltip: 'Specifies to only sample from the top K options for each subsequent token.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
'alibabacloud-ai-search': [
{
task_type: 'text_embedding',
configuration: {
input_type: {
display: DisplayType.DROPDOWN,
label: 'Input type',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the type of input passed to the model.',
type: FieldType.STRING,
validations: [],
options: [
{
label: 'ingest',
value: 'ingest',
},
{
label: 'search',
value: 'search',
},
],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'sparse_embedding',
configuration: {
input_type: {
display: DisplayType.DROPDOWN,
label: 'Input type',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the type of input passed to the model.',
type: FieldType.STRING,
validations: [],
options: [
{
label: 'ingest',
value: 'ingest',
},
{
label: 'search',
value: 'search',
},
],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
return_token: {
display: DisplayType.TOGGLE,
label: 'Return token',
options: [],
order: 1,
required: false,
sensitive: false,
tooltip:
'If `true`, the token name will be returned in the response. Defaults to `false` which means only the token ID will be returned in the response.',
type: FieldType.BOOLEAN,
validations: [],
value: true,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
{
task_type: 'completion',
configuration: {},
},
{
task_type: 'rerank',
configuration: {},
},
],
watsonxai: [
{
task_type: 'text_embedding',
configuration: {},
},
],
};
return Promise.resolve(providersTaskTypes[provider]);
};

View file

@ -7,7 +7,7 @@
import { isEmpty } from 'lodash/fp';
import { ValidationFunc } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { ConfigEntryView } from '../lib/dynamic_config/types';
import { ConfigEntryView } from '../../../common/dynamic_config/types';
import { Config } from './types';
import * as i18n from './translations';

View file

@ -8,8 +8,8 @@
import React from 'react';
import { HiddenField } from '@kbn/es-ui-shared-plugin/static/forms/components';
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
import { ConfigEntryView } from '../../../common/dynamic_config/types';
import { getNonEmptyValidator } from './helpers';
import { ConfigEntryView } from '../lib/dynamic_config/types';
export const getProviderSecretsHiddenField = (
providerSchema: ConfigEntryView[],

View file

@ -26,7 +26,7 @@ interface ServiceProviderProps {
searchValue?: string;
}
type ProviderSolution = 'Observability' | 'Security' | 'Search';
export type ProviderSolution = 'Observability' | 'Security' | 'Search';
interface ServiceProviderRecord {
icon: string;
@ -107,9 +107,7 @@ export const ServiceProviderIcon: React.FC<ServiceProviderProps> = ({ providerKe
return provider ? (
<EuiIcon data-test-subj={`icon-service-provider-${providerKey}`} type={provider.icon} />
) : (
<span>{providerKey}</span>
);
) : null;
};
export const ServiceProviderName: React.FC<ServiceProviderProps> = ({

View file

@ -11,6 +11,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { ServiceProviderKeys } from '../../../../../common/inference/constants';
import {
ProviderSolution,
SERVICE_PROVIDERS,
ServiceProviderIcon,
ServiceProviderName,
@ -47,7 +48,20 @@ const SelectableProviderComponent: React.FC<SelectableProviderProps> = ({
const renderProviderOption = useCallback<NonNullable<EuiSelectableProps['renderOption']>>(
(option, searchValue) => {
const provider = SERVICE_PROVIDERS[option.label as ServiceProviderKeys];
const provider = Object.keys(SERVICE_PROVIDERS).includes(option.label)
? SERVICE_PROVIDERS[option.label as ServiceProviderKeys]
: undefined;
const supportedBySolutions = (provider &&
provider.solutions.map((solution) => (
<EuiFlexItem>
<EuiBadge color="hollow">{solution}</EuiBadge>
</EuiFlexItem>
))) ?? (
<EuiFlexItem>
<EuiBadge color="hollow">{'Search' as ProviderSolution}</EuiBadge>
</EuiFlexItem>
);
return (
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>
@ -65,12 +79,7 @@ const SelectableProviderComponent: React.FC<SelectableProviderProps> = ({
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" responsive={false}>
{provider &&
provider.solutions.map((solution) => (
<EuiFlexItem>
<EuiBadge color="hollow">{solution}</EuiBadge>
</EuiFlexItem>
))}
{supportedBySolutions}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -14,7 +14,6 @@ import {
SparseEmbeddingParams,
TextEmbeddingParams,
} from '../../../common/inference/types';
import { ConfigProperties } from '../lib/dynamic_config/types';
export type InferenceActionParams =
| { subAction: SUB_ACTION.COMPLETION; subActionParams: ChatCompleteParams }
@ -22,8 +21,6 @@ export type InferenceActionParams =
| { subAction: SUB_ACTION.SPARSE_EMBEDDING; subActionParams: SparseEmbeddingParams }
| { subAction: SUB_ACTION.TEXT_EMBEDDING; subActionParams: TextEmbeddingParams };
export type FieldsConfiguration = Record<string, ConfigProperties>;
export interface Config {
taskType: string;
taskTypeConfig?: Record<string, unknown>;

View file

@ -25,12 +25,12 @@ import {
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import { ConfigEntryView, DisplayType } from '../../../../common/dynamic_config/types';
import {
ensureBooleanType,
ensureCorrectTyping,
ensureStringType,
} from './connector_configuration_utils';
import { ConfigEntryView, DisplayType } from './types';
interface ConnectorConfigurationFieldProps {
configEntry: ConfigEntryView;

View file

@ -18,7 +18,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ConfigEntryView, DisplayType } from './types';
import { ConfigEntryView, DisplayType } from '../../../../common/dynamic_config/types';
import { ConnectorConfigurationField } from './connector_configuration_field';
interface ConnectorConfigurationFormItemsProps {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ConfigProperties, FieldType } from './types';
import { ConfigProperties, FieldType } from '../../../../common/dynamic_config/types';
export type ConnectorConfigEntry = ConfigProperties & { key: string };

View file

@ -8,7 +8,11 @@
import { PluginInitializerContext, Plugin, CoreSetup, Logger } from '@kbn/core/server';
import { PluginSetupContract as ActionsPluginSetupContract } from '@kbn/actions-plugin/server';
import { registerConnectorTypes } from './connector_types';
import { validSlackApiChannelsRoute, getWellKnownEmailServiceRoute } from './routes';
import {
validSlackApiChannelsRoute,
getWellKnownEmailServiceRoute,
getInferenceServicesRoute,
} from './routes';
import {
ExperimentalFeatures,
parseExperimentalConfigValue,
@ -39,6 +43,7 @@ export class StackConnectorsPlugin implements Plugin<void, void> {
getWellKnownEmailServiceRoute(router);
validSlackApiChannelsRoute(router, actions.getActionsConfigurationUtilities(), this.logger);
getInferenceServicesRoute(router);
registerConnectorTypes({
actions,

View file

@ -0,0 +1,133 @@
/*
* 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 { httpServiceMock, httpServerMock } from '@kbn/core/server/mocks';
import { coreMock } from '@kbn/core/server/mocks';
import { getInferenceServicesRoute } from './get_inference_services';
import { DisplayType, FieldType } from '../../common/dynamic_config/types';
describe('getInferenceServicesRoute', () => {
it('returns available service providers', async () => {
const router = httpServiceMock.createRouter();
const core = coreMock.createRequestHandlerContext();
const mockResult = [
{
provider: 'openai',
task_types: [
{
task_type: 'completion',
configuration: {
user: {
display: DisplayType.TEXTBOX,
label: 'User',
order: 1,
required: false,
sensitive: false,
tooltip: 'Specifies the user issuing the request.',
type: FieldType.STRING,
validations: [],
value: '',
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
],
configuration: {
api_key: {
display: DisplayType.TEXTBOX,
label: 'API Key',
order: 3,
required: true,
sensitive: true,
tooltip: `The OpenAI API authentication key. For more details about generating OpenAI API keys, refer to the https://platform.openai.com/account/api-keys.`,
type: FieldType.STRING,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
model_id: {
display: DisplayType.TEXTBOX,
label: 'Model ID',
order: 2,
required: true,
sensitive: false,
tooltip: 'The name of the model to use for the inference task.',
type: FieldType.STRING,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
organization_id: {
display: DisplayType.TEXTBOX,
label: 'Organization ID',
order: 4,
required: false,
sensitive: false,
tooltip: 'The unique identifier of your organization.',
type: FieldType.STRING,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
url: {
display: DisplayType.TEXTBOX,
label: 'URL',
order: 1,
required: true,
sensitive: false,
tooltip:
'The OpenAI API endpoint URL. For more information on the URL, refer to the https://platform.openai.com/docs/api-reference.',
type: FieldType.STRING,
validations: [],
value: null,
ui_restrictions: [],
default_value: 'https://api.openai.com/v1/chat/completions',
depends_on: [],
},
'rate_limit.requests_per_minute': {
display: DisplayType.NUMERIC,
label: 'Rate limit',
order: 5,
required: false,
sensitive: false,
tooltip:
'Default number of requests allowed per minute. For text_embedding is 3000. For completion is 500.',
type: FieldType.INTEGER,
validations: [],
value: null,
ui_restrictions: [],
default_value: null,
depends_on: [],
},
},
},
];
core.elasticsearch.client.asInternalUser.transport.request.mockResolvedValue(mockResult);
getInferenceServicesRoute(router);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/internal/stack_connectors/_inference/_services"`);
const mockResponse = httpServerMock.createResponseFactory();
const mockRequest = httpServerMock.createKibanaRequest();
await handler({ core }, mockRequest, mockResponse);
expect(mockResponse.ok).toHaveBeenCalledWith({
body: mockResult,
});
});
});

View file

@ -0,0 +1,48 @@
/*
* 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,
RequestHandlerContext,
KibanaRequest,
IKibanaResponse,
KibanaResponseFactory,
} from '@kbn/core/server';
import { InferenceProvider } from '../../common/inference/types';
import { INTERNAL_BASE_STACK_CONNECTORS_API_PATH } from '../../common';
export const getInferenceServicesRoute = (router: IRouter) => {
router.get(
{
path: `${INTERNAL_BASE_STACK_CONNECTORS_API_PATH}/_inference/_services`,
options: {
access: 'internal',
},
validate: false,
},
handler
);
async function handler(
ctx: RequestHandlerContext,
req: KibanaRequest<unknown, unknown, unknown>,
res: KibanaResponseFactory
): Promise<IKibanaResponse> {
const esClient = (await ctx.core).elasticsearch.client.asInternalUser;
const response = await esClient.transport.request<{
endpoints: InferenceProvider[];
}>({
method: 'GET',
path: `/_inference/_services`,
});
return res.ok({
body: response,
});
}
};

View file

@ -7,3 +7,4 @@
export { getWellKnownEmailServiceRoute } from './get_well_known_email_service';
export { validSlackApiChannelsRoute } from './valid_slack_api_channels';
export { getInferenceServicesRoute } from './get_inference_services';