From 78b83df4056146e3de011d6025482e3a8a1df0a2 Mon Sep 17 00:00:00 2001 From: Shibesh Duwadi Date: Wed, 16 Oct 2024 10:21:35 +0200 Subject: [PATCH] changes --- sample-code/src/ai-api/deployment-api.ts | 107 ++++++++++-------- sample-code/src/index.ts | 5 +- sample-code/src/server.ts | 57 +++++++++- tests/e2e-tests/package.json | 5 +- tests/e2e-tests/src/deployment-api.test.ts | 55 +++++---- tests/e2e-tests/src/utils/ai-api-utils.ts | 13 ++- .../src/utils/cleanup-deployments.ts | 30 ++--- 7 files changed, 164 insertions(+), 108 deletions(-) diff --git a/sample-code/src/ai-api/deployment-api.ts b/sample-code/src/ai-api/deployment-api.ts index 448b7db4..f89c4441 100644 --- a/sample-code/src/ai-api/deployment-api.ts +++ b/sample-code/src/ai-api/deployment-api.ts @@ -1,28 +1,22 @@ import { DeploymentApi } from '@sap-ai-sdk/ai-api'; import type { + AiDeploymentBulkModificationResponse, AiDeploymentCreationResponse, AiDeploymentDeletionResponse, AiDeploymentList, - AiDeploymentModificationResponse, - AiDeploymentResponseWithDetails + AiDeploymentModificationRequestList, + AiDeploymentStatus } from '@sap-ai-sdk/ai-api'; /** - * Get all deployments. + * Get all deployments filtered by status. * @param resourceGroup - AI-Resource-Group where the resources are available. - * @param status - Optional parameter to filter deployments by status. - * @returns All deployments. + * @param status - Optional query parameter to filter deployments by status. + * @returns List of deployments. */ export async function getDeployments( resourceGroup: string, - status?: - | 'PENDING' - | 'RUNNING' - | 'COMPLETED' - | 'DEAD' - | 'STOPPING' - | 'STOPPED' - | 'UNKNOWN' + status?: AiDeploymentStatus ): Promise { // check for optional query parameters. const queryParams = status ? { status } : {}; @@ -31,23 +25,6 @@ export async function getDeployments( }).execute(); } -/** - * Get information about specific deployment. - * @param deploymentId - ID of the specific deployment. - * @param resourceGroup - AI-Resource-Group where the resources are available. - * @returns Details for deplyoment with deploymentId. - */ -export async function getDeployment( - deploymentId: string, - resourceGroup: string -): Promise { - return DeploymentApi.deploymentGet( - deploymentId, - {}, - { 'AI-Resource-Group': resourceGroup } - ).execute(); -} - /** * Create a deployment using the configuration specified by configurationId. * @param configurationId - ID of the configuration to be used. @@ -65,35 +42,67 @@ export async function createDeployment( } /** - * Update target status of a specific deployment to stop it. + * Stop all deployments with the specific configuration ID. * Only deployments with 'status': 'RUNNING' can be stopped. - * @param deploymentId - ID of the specific deployment. + * @param configurationId - ID of the configuration to be used. * @param resourceGroup - AI-Resource-Group where the resources are available. - * @returns Deployment modification response with 'targetStatus': 'STOPPED'. + * @returns Deployment modification response list with 'targetStatus': 'STOPPED'. */ -export async function stopDeployment( - deploymentId: string, +export async function stopDeployments( + configurationId: string, resourceGroup: string -): Promise { - return DeploymentApi.deploymentModify( - deploymentId, - { targetStatus: 'STOPPED' }, +): Promise { + // Get all RUNNING deployments with configurationId + const deployments: AiDeploymentList = await DeploymentApi.deploymentQuery( + { status: 'RUNNING', configurationId }, + { 'AI-Resource-Group': resourceGroup } + ).execute(); + + // Map the deployment Ids and add property targetStatus: 'STOPPED' + const deploymentsToStop: any = deployments.resources.map(deployment => ({ + id: deployment.id, + targetStatus: 'STOPPED' + })); + + // Send batch modify request to stop deployments + return DeploymentApi.deploymentBatchModify( + { deployments: deploymentsToStop as AiDeploymentModificationRequestList }, { 'AI-Resource-Group': resourceGroup } ).execute(); } /** - * Mark deployment with deploymentId as deleted. - * Only deployments with 'status': 'STOPPED' can be deleted. - * @param deploymentId - ID of the specific deployment. + * Delete all deployments. + * Only deployments with 'status': 'STOPPED' and 'status': 'UNKNOWN' can be deleted. * @param resourceGroup - AI-Resource-Group where the resources are available. - * @returns Deployment deletion response with 'targetStatus': 'DELETED'. + * @returns Deployment deletion response list with 'targetStatus': 'DELETED'. */ -export async function deleteDeployment( - deploymentId: string, +export async function deleteDeployments( resourceGroup: string -): Promise { - return DeploymentApi.deploymentDelete(deploymentId, { - 'AI-Resource-Group': resourceGroup - }).execute(); +): Promise { + // Get all STOPPED and UNKNOWN deployments + const [runningDeployments, unknownDeployments] = await Promise.all([ + DeploymentApi.deploymentQuery( + { status: 'STOPPED' }, + { 'AI-Resource-Group': resourceGroup } + ).execute(), + DeploymentApi.deploymentQuery( + { status: 'UNKNOWN' }, + { 'AI-Resource-Group': resourceGroup } + ).execute() + ]); + + const deploymentsToDelete = [ + ...runningDeployments.resources, + ...unknownDeployments.resources + ]; + + // Delete all deployments + return Promise.all( + deploymentsToDelete.map(deployment => + DeploymentApi.deploymentDelete(deployment.id, { + 'AI-Resource-Group': resourceGroup + }).execute() + ) + ); } diff --git a/sample-code/src/index.ts b/sample-code/src/index.ts index 584673da..2edd6714 100644 --- a/sample-code/src/index.ts +++ b/sample-code/src/index.ts @@ -18,11 +18,10 @@ export { invokeRagChain } from './langchain-azure-openai.js'; export { - getDeployment, getDeployments, createDeployment, - stopDeployment, - deleteDeployment + stopDeployments, + deleteDeployments // eslint-disable-next-line import/no-internal-modules } from './ai-api/deployment-api.js'; export { diff --git a/sample-code/src/server.ts b/sample-code/src/server.ts index 36074d78..5e22d760 100644 --- a/sample-code/src/server.ts +++ b/sample-code/src/server.ts @@ -14,10 +14,13 @@ import { } from './orchestration.js'; import { getDeployments, - createDeployment + createDeployment, + stopDeployments, + deleteDeployments // eslint-disable-next-line import/no-internal-modules } from './ai-api/deployment-api.js'; import { + getScenarios, getModelsInScenario // eslint-disable-next-line import/no-internal-modules } from './ai-api/scenario-api.js'; @@ -26,7 +29,7 @@ import { invokeRagChain, invoke } from './langchain-azure-openai.js'; -import type { AiApiError } from '@sap-ai-sdk/ai-api'; +import type { AiApiError, AiDeploymentStatus } from '@sap-ai-sdk/ai-api'; import type { OrchestrationResponse } from '@sap-ai-sdk/orchestration'; const app = express(); @@ -96,9 +99,11 @@ app.get('/orchestration/:sampleCase', async (req, res) => { } }); -app.get('/ai-api/get-deployments', async (req, res) => { +app.get('/ai-api/deployments', async (req, res) => { try { - res.send(await getDeployments('default')); + res.send( + await getDeployments('default', req.query.status as AiDeploymentStatus) + ); } catch (error: any) { console.error(error); const apiError = error.response.data.error as AiApiError; @@ -108,7 +113,7 @@ app.get('/ai-api/get-deployments', async (req, res) => { } }); -app.post('/ai-api/create-deployment', async (req, res) => { +app.post('/ai-api/deployment/create', express.json(), async (req, res) => { try { res.send(await createDeployment(req.body.configurationId, 'default')); } catch (error: any) { @@ -120,7 +125,47 @@ app.post('/ai-api/create-deployment', async (req, res) => { } }); -app.get('/ai-api/get-models-in-scenario', async (req, res) => { +app.patch('/ai-api/deployment/batch-stop', express.json(), async (req, res) => { + try { + res.send(await stopDeployments(req.body.configurationId, 'default')); + } catch (error: any) { + console.error(error); + const apiError = error.response.data.error as AiApiError; + res + .status(error.response.status) + .send('Yikes, vibes are off apparently 😬 -> ' + apiError.message); + } +}); + +app.delete( + '/ai-api/deployment/batch-delete', + express.json(), + async (req, res) => { + try { + res.send(await deleteDeployments('default')); + } catch (error: any) { + console.error(error); + const apiError = error.response.data.error as AiApiError; + res + .status(error.response.status) + .send('Yikes, vibes are off apparently 😬 -> ' + apiError.message); + } + } +); + +app.get('/ai-api/scenarios', async (req, res) => { + try { + res.send(await getScenarios('default')); + } catch (error: any) { + console.error(error); + const apiError = error.response.data.error as AiApiError; + res + .status(error.response.status) + .send('Yikes, vibes are off apparently 😬 -> ' + apiError.message); + } +}); + +app.get('/ai-api/models', async (req, res) => { try { res.send(await getModelsInScenario('foundation-models', 'default')); } catch (error: any) { diff --git a/tests/e2e-tests/package.json b/tests/e2e-tests/package.json index e271892f..bc664404 100644 --- a/tests/e2e-tests/package.json +++ b/tests/e2e-tests/package.json @@ -19,10 +19,7 @@ }, "scripts": { "compile": "tsc", - "test": "NODE_OPTIONS=--experimental-vm-modules jest", - "test:deployment-api-create": "NODE_OPTIONS=--experimental-vm-modules jest -t \"create deployment\"", - "test:deployment-api-stop": "NODE_OPTIONS=--experimental-vm-modules jest -t \"stop deployment\"", - "test:deployment-api-delete": "NODE_OPTIONS=--experimental-vm-modules jest -t \"delete deployment\"", + "test": "NODE_OPTIONS=--experimental-vm-modules jest deployment-api.test.ts", "cleanup-deployments": "node --loader ts-node/esm ./src/utils/cleanup-deployments.ts", "lint": "eslint . && prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -c", "lint:fix": "eslint . --fix && prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -w --log-level error" diff --git a/tests/e2e-tests/src/deployment-api.test.ts b/tests/e2e-tests/src/deployment-api.test.ts index 94d16b46..69906c4d 100644 --- a/tests/e2e-tests/src/deployment-api.test.ts +++ b/tests/e2e-tests/src/deployment-api.test.ts @@ -1,10 +1,5 @@ -import { - getDeployment, - getDeployments, - createDeployment, - stopDeployment, - deleteDeployment -} from '@sap-ai-sdk/sample-code'; +import { DeploymentApi } from '@sap-ai-sdk/ai-api'; +import { getDeployments, createDeployment } from '@sap-ai-sdk/sample-code'; import { loadEnv } from './utils/load-env.js'; import { configurationId, @@ -17,12 +12,12 @@ loadEnv(); describe('DeploymentApi', () => { let createdDeploymentId: string | undefined; - let initialState: AiDeploymentList | undefined; + let initialDeployments: AiDeploymentList | undefined; beforeAll(async () => { const queryResponse = await getDeployments(resourceGroup); expect(queryResponse).toBeDefined(); - initialState = queryResponse; + initialDeployments = queryResponse; }); it('should create a deployment and wait for it to run', async () => { @@ -57,7 +52,11 @@ describe('DeploymentApi', () => { 'RUNNING' ); - const modifyResponse = await stopDeployment(deploymentId, resourceGroup); + const modifyResponse = await DeploymentApi.deploymentModify( + deploymentId, + { targetStatus: 'STOPPED' }, + { 'AI-Resource-Group': resourceGroup } + ).execute(); expect(modifyResponse).toEqual( expect.objectContaining({ message: 'Deployment modification scheduled' @@ -81,7 +80,9 @@ describe('DeploymentApi', () => { 'STOPPED' ); - const deleteResponse = await deleteDeployment(deploymentId, resourceGroup); + const deleteResponse = await DeploymentApi.deploymentDelete(deploymentId, { + 'AI-Resource-Group': resourceGroup + }).execute(); expect(deleteResponse).toEqual( expect.objectContaining({ message: 'Deletion scheduled' @@ -90,33 +91,39 @@ describe('DeploymentApi', () => { // Wait for deletion to complete await new Promise(r => setTimeout(r, 30000)); - await expect(getDeployment(deploymentId, resourceGroup)).rejects.toThrow(); - }, 100000); + await expect( + DeploymentApi.deploymentGet( + deploymentId, + {}, + { 'AI-Resource-Group': resourceGroup } + ).execute() + ).rejects.toThrow(); + }, 150000); it('should validate consistency of deployments after test flow', async () => { const queryResponse = await getDeployments(resourceGroup); expect(queryResponse).toBeDefined(); - const sanitizedInitialState = sanitizedState( - initialState, + const initialFilteredDeployments = filterDeployments( + initialDeployments, createdDeploymentId ); - const sanitizedEndState = sanitizedState( + const finalFilteredDeployments = filterDeployments( queryResponse, createdDeploymentId ); - expect(sanitizedEndState.resources).toStrictEqual( - sanitizedInitialState.resources + expect(finalFilteredDeployments.resources).toStrictEqual( + initialFilteredDeployments.resources ); }); }); -const sanitizedState = ( - state: AiDeploymentList | undefined, +const filterDeployments = ( + deployments: AiDeploymentList | undefined, createdDeployentId: string | undefined ) => ({ - ...state, - resources: state?.resources + ...deployments, + resources: deployments?.resources .filter(deployment => deployment.id === createdDeployentId) // eslint-disable-next-line @typescript-eslint/no-unused-vars .map(({ modifiedAt, ...rest }) => rest) @@ -126,10 +133,10 @@ async function checkCreatedDeployment( deploymentId: string | undefined, status: 'RUNNING' | 'STOPPED' ): Promise { - if (deploymentId === undefined) { + if (!deploymentId) { try { const response = await getDeployments(resourceGroup, status); - if (response.count === 0) { + if (!response.count) { throw new Error( `No ${status} deployments found, please ${status === 'RUNNING' ? 'create' : 'stop'} a deployment first to ${status === 'RUNNING' ? 'modify' : 'delete'} it.` ); diff --git a/tests/e2e-tests/src/utils/ai-api-utils.ts b/tests/e2e-tests/src/utils/ai-api-utils.ts index 5889750f..988910dd 100644 --- a/tests/e2e-tests/src/utils/ai-api-utils.ts +++ b/tests/e2e-tests/src/utils/ai-api-utils.ts @@ -1,5 +1,5 @@ import retry from 'async-retry'; -import { getDeployment } from '@sap-ai-sdk/sample-code'; +import { DeploymentApi } from '@sap-ai-sdk/ai-api'; import type { AiDeployment } from '@sap-ai-sdk/ai-api'; /** @@ -21,15 +21,20 @@ export async function waitForDeploymentToReachStatus( ): Promise { return retry( async () => { - const deploymentDetail = await getDeployment(deploymentId, resourceGroup); + const deploymentDetail = await DeploymentApi.deploymentGet( + deploymentId, + {}, + { 'AI-Resource-Group': resourceGroup } + ).execute(); if (deploymentDetail.status === targetStatus) { return deploymentDetail; } throw new Error(`Deployment has not yet reached ${targetStatus} status.`); }, { - retries: 30, - minTimeout: 5000 + retries: 20, + minTimeout: 5000, + factor: 1 } ); } diff --git a/tests/e2e-tests/src/utils/cleanup-deployments.ts b/tests/e2e-tests/src/utils/cleanup-deployments.ts index 1e3f0aac..304b48c6 100644 --- a/tests/e2e-tests/src/utils/cleanup-deployments.ts +++ b/tests/e2e-tests/src/utils/cleanup-deployments.ts @@ -1,9 +1,6 @@ import { createLogger } from '@sap-cloud-sdk/util'; -import { - deleteDeployment, - getDeployments, - stopDeployment -} from '@sap-ai-sdk/sample-code'; +import { getDeployments } from '@sap-ai-sdk/sample-code'; +import { DeploymentApi } from '@sap-ai-sdk/ai-api'; import { loadEnv } from './load-env.js'; import { resourceGroup, @@ -28,21 +25,18 @@ async function cleanupDeployments(): Promise { logger.info('Starting deployment cleanup process.'); await Promise.all( deployments.resources.map(async deployment => { - const { id, status, targetStatus } = deployment; - if ( - status !== 'STOPPED' && - targetStatus !== 'STOPPED' && - status !== 'UNKNOWN' - ) { - await stopDeployment(id, resourceGroup); - await waitForDeploymentToReachStatus(id, 'STOPPED'); - } else if (status !== 'STOPPED' && targetStatus === 'STOPPED') { + const { id, status } = deployment; + if (status !== 'STOPPED' && status !== 'UNKNOWN') { + await DeploymentApi.deploymentModify( + id, + { targetStatus: 'STOPPED' }, + { 'AI-Resource-Group': resourceGroup } + ).execute(); await waitForDeploymentToReachStatus(id, 'STOPPED'); } - - await deleteDeployment(id, resourceGroup); - // Wait for deletion to complete - await new Promise(r => setTimeout(r, 25000)); + await DeploymentApi.deploymentDelete(id, { + 'AI-Resource-Group': resourceGroup + }).execute(); }) ); logger.info('Deployment cleanup successful.');