Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AI API create flow and Backend Rate Limiting #744

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion portals/publisher/src/main/webapp/site/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@
"Apis.APIProductCreateWrapper.error.errorMessage.create.api.product": "Something went wrong while adding the API Product",
"Apis.APIProductCreateWrapper.error.errorMessage.create.revision": "Something went wrong while creating the API Product Revision",
"Apis.APIProductCreateWrapper.error.errorMessage.deploy.revision": "Something went wrong while deploying the API Product Revision",
"Apis.Create.AIAPI.ApiCreateAIAPI.back": "Back",
"Apis.Create.AIAPI.ApiCreateAIAPI.cancel": "Cancel",
"Apis.Create.AIAPI.ApiCreateAIAPI.create": "Create",
"Apis.Create.AIAPI.ApiCreateAIAPI.heading": "Create an API using an AI provider API definition.",
"Apis.Create.AIAPI.ApiCreateAIAPI.next": "Next",
"Apis.Create.AIAPI.ApiCreateAIAPI.sub.heading": "Create an API using an existing AI provider API definition.",
"Apis.Create.AIAPI.ApiCreateAIAPI.wizard.one": "Provide AI provider API",
"Apis.Create.AIAPI.ApiCreateAIAPI.wizard.two": "Create API",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model": "API version",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.empty": "No API Provider selected.",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.helper": "Select API Model version for the API",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.model.placeholder": "Search API version",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider": "API Provider",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.empty": "Loading API Providers...",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.helper.text": "Select AI API Provider for the API",
"Apis.Create.AIAPI.Steps.ProvideAIOpenAPI.AI.provider.placeholder": "Search AI API Provider",
"Apis.Create.APIProduct.APIProductCreateWrapper.back": "Back",
"Apis.Create.APIProduct.APIProductCreateWrapper.cancel": "Cancel",
"Apis.Create.APIProduct.APIProductCreateWrapper.create": "Create",
Expand Down Expand Up @@ -345,6 +361,8 @@
"Apis.Details.Configurartion.components.QueryAnalysis.update.complexity": "update complexity",
"Apis.Details.Configuration.ApiKeyHeader.helper.text": "ApiKey header name cannot contain spaces or special characters",
"Apis.Details.Configuration.AuthHeader.helper.text": "Authorization header name cannot contain spaces or special characters",
"Apis.Details.Configuration.Components.AI.BE.Rate.Limiting.prod": "Backend Rate Limiting",
"Apis.Details.Configuration.Components.AI.BE.Rate.Limiting.tooltip": "This option determines the type of Backend Rate Limiting that is applied to the API.",
"Apis.Details.Configuration.Components.APISecurity.Components.\n ApplicationLevel.Client.Websocket": "Client Websocket",
"Apis.Details.Configuration.Components.APISecurity.Components.\n ApplicationLevel.Websocket": "Application Level Security",
"Apis.Details.Configuration.Components.APISecurity.Components.ApplicationLevel.http": "Application Level Security",
Expand Down Expand Up @@ -1510,6 +1528,7 @@
"Apis.Details.TryOutConsole.token.helper": "Generate or provide an internal API Key",
"Apis.Details.TryOutConsole.token.label": "Internal API Key",
"Apis.Details.components.APIDetailsTopMenu.advertise.only.label": "Third Party",
"Apis.Details.components.APIDetailsTopMenu.ai.api.label": "AI API",
"Apis.Details.components.APIDetailsTopMenu.created.by": "Created by:",
"Apis.Details.components.APIDetailsTopMenu.current.api": "Current API",
"Apis.Details.components.APIDetailsTopMenu.error": "Something went wrong while downloading the API.",
Expand Down Expand Up @@ -1582,6 +1601,8 @@
"Apis.Listing.ApiThumb.owners.technical": "Technical",
"Apis.Listing.ApiThumb.version": "Version",
"Apis.Listing.Components.Create.API": "Create API",
"Apis.Listing.SampleAPI.SampleAPI.ai.api.create.title": "Create AI API",
"Apis.Listing.SampleAPI.SampleAPI.ai.api.import.content": "Create AI APIs by importing AI vendor APIs",
"Apis.Listing.SampleAPI.SampleAPI.create.new": "Let’s get started !",
"Apis.Listing.SampleAPI.SampleAPI.create.new.description": "Choose your option to create an API",
"Apis.Listing.SampleAPI.SampleAPI.graphql.api": "GraphQL",
Expand Down Expand Up @@ -2030,4 +2051,4 @@
"upload.image": "Click or drag the image to upload.",
"upload.image.size.error": "Uploaded File is too large. Maximum file size limit to 1MB",
"upload.image.size.info": "Maximum file size limit to 1MB"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
/*
* Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useReducer, useState } from 'react';
import PropTypes from 'prop-types';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import { FormattedMessage, useIntl } from 'react-intl';
import { usePublisherSettings } from 'AppComponents/Shared/AppContext';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import Button from '@mui/material/Button';
import { Link } from 'react-router-dom';
import API from 'AppData/api';
import Alert from 'AppComponents/Shared/Alert';
import CircularProgress from '@mui/material/CircularProgress';
import DefaultAPIForm from 'AppComponents/Apis/Create/Components/DefaultAPIForm';
import APICreateBase from 'AppComponents/Apis/Create/Components/APICreateBase';
import ProvideAIOpenAPI from './Steps/ProvideAIOpenAPI';


/**
*
* Reduce the events triggered from API input fields to current state
* @param {*} currentState
* @param {*} inputAction
* @returns
*/
function apiInputsReducer(currentState, inputAction) {
const { action, value } = inputAction;
switch (action) {
case 'type':
case 'inputValue':
case 'name':
case 'version':
case 'endpoint':
case 'gatewayType':
case 'context':
case 'policies':
case 'llmProviderName':
case 'llmProviderApiVersion':
case 'isFormValid':
return { ...currentState, [action]: value };
case 'preSetAPI':
return {
...currentState,
name: value.name.replace(/[&/\\#,+()$~%.'":*?<>{}\s]/g, ''),
version: value.version,
context: value.context,
endpoint: value.endpoints && value.endpoints[0],
};
default:
return currentState;
}
}
/**
* Handle API creation from AI Provider API Definition.
*
* @export
* @param {*} props
* @returns
*/
export default function ApiCreateAIAPI(props) {
const [wizardStep, setWizardStep] = useState(0);
const { history, multiGateway } = props;
const { data: settings } = usePublisherSettings();

const [apiInputs, inputsDispatcher] = useReducer(apiInputsReducer, {
type: 'ApiCreateAIAPI',
inputValue: '',
formValidity: false,
});

const intl = useIntl();

/**
*
*
* @param {*} event
*/
function handleOnChange(event) {
const { name: action, value } = event.target;
inputsDispatcher({ action, value });
}

/**
*
* Set the validity of the API Inputs form
* @param {*} isValidForm
* @param {*} validationState
*/
function handleOnValidate(isFormValid) {
inputsDispatcher({
action: 'isFormValid',
value: isFormValid,
});
}

const [isCreating, setCreating] = useState();
/**
*
*
* @param {*} params
*/
function createAPI() {
setCreating(true);
const {
name, version, context, endpoint, gatewayType, policies = ["Unlimited"], inputValue,
llmProviderName, llmProviderApiVersion,
} = apiInputs;
let defaultGatewayType;
if (settings && settings.gatewayTypes.length === 1 && settings.gatewayTypes.includes('Regular')) {
defaultGatewayType = 'wso2/synapse';
} else if (settings && settings.gatewayTypes.length === 1 && settings.gatewayTypes.includes('APK')) {
defaultGatewayType = 'wso2/apk';
} else {
defaultGatewayType = 'default';
}

const additionalProperties = {
name,
version,
context,
gatewayType: defaultGatewayType === 'default' ? gatewayType : defaultGatewayType,
policies,
aiConfiguration: {
llmProviderName,
llmProviderApiVersion,
},
};
if (endpoint) {
additionalProperties.endpointConfig = {
endpoint_type: 'http',
sandbox_endpoints: {
url: endpoint,
},
production_endpoints: {
url: endpoint,
},
};
}
const newAPI = new API(additionalProperties);
const promisedResponse = newAPI.importOpenAPIByInlineDefinition(inputValue);
promisedResponse
.then((api) => {
Alert.info(intl.formatMessage({
id: 'Apis.Create.OpenAPI.ApiCreateOpenAPI.created.success',
defaultMessage: 'API created successfully',
}));
history.push(`/apis/${api.id}/overview`);
})
.catch((error) => {
if (error.response) {
Alert.error(error.response.body.description);
} else {
Alert.error(intl.formatMessage({
id: 'Apis.Create.OpenAPI.ApiCreateOpenAPI.created.error',
defaultMessage: 'Something went wrong while adding the API',
}));
}
console.error(error);
})
.finally(() => setCreating(false));
}

return (
<APICreateBase
title={(
<>
<Typography variant='h5'>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.heading'
defaultMessage='Create an API using an AI provider API definition.'
/>
</Typography>
<Typography variant='caption'>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.sub.heading'
defaultMessage='Create an API using an existing AI provider API definition.'
/>
</Typography>
</>
)}
>
<Box sx={{ mb: 2 }}>
<Stepper alternativeLabel activeStep={0}>
<Step>
<StepLabel>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.wizard.one'
defaultMessage='Provide AI provider API'
/>
</StepLabel>
</Step>

<Step>
<StepLabel>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.wizard.two'
defaultMessage='Create API'
/>
</StepLabel>
</Step>
</Stepper>
</Box>

<Grid container spacing={2}>
<Grid item xs={12}>
{wizardStep === 0 && (
<ProvideAIOpenAPI
onValidate={handleOnValidate}
apiInputs={apiInputs}
inputsDispatcher={inputsDispatcher}
/>
)}
{wizardStep === 1 && (
<DefaultAPIForm
onValidate={handleOnValidate}
onChange={handleOnChange}
multiGateway={multiGateway}
api={apiInputs}
isAPIProduct={false}
/>
)}
</Grid>
<Grid item xs={12}>
<Grid container direction='row' justifyContent='flex-start' alignItems='center' spacing={2}>
<Grid item>
{wizardStep === 0 && (
<Link to='/apis/'>
<Button>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.cancel'
defaultMessage='Cancel'
/>
</Button>
</Link>
)}
{wizardStep === 1 && (
<Button onClick={() => setWizardStep((step) => step - 1)}>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.back'
defaultMessage='Back'
/>
</Button>
)}
</Grid>
<Grid item>
{wizardStep === 0 && (
<Button
onClick={() => setWizardStep((step) => step + 1)}
variant='contained'
color='primary'
disabled={!apiInputs.isFormValid}
id='ai-api-create-next-btn'
>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.next'
defaultMessage='Next'
/>
</Button>
)}
{wizardStep === 1 && (
<Button
variant='contained'
color='primary'
disabled={!apiInputs.isFormValid || isCreating}
onClick={createAPI}
id='ai-api-create-btn'
>
<FormattedMessage
id='Apis.Create.AIAPI.ApiCreateAIAPI.create'
defaultMessage='Create'
/>
{' '}
{isCreating && <CircularProgress size={24} />}
</Button>
)}
</Grid>
</Grid>
</Grid>
</Grid>
</APICreateBase>
);
}

ApiCreateAIAPI.propTypes = {
history: PropTypes.shape({ push: PropTypes.func }).isRequired,
multiGateway: PropTypes.string.isRequired,
};
Loading