Skip to content

Commit

Permalink
chore: Refactor ai-api E2E tests (#187)
Browse files Browse the repository at this point in the history
* add sample code

* refactor tests and add cleanup

* add changelog

* update docs

* fix: Changes from lint

* changes

* remove changelog, update README

* fix: Changes from lint

* restructure

* add cleanup script

* missing dep

* whoops

* changes to sample code

* rename

* missed one

* lint -_-

* changes

* lockfile

* forgot to change this

* optimize

---------

Co-authored-by: cloud-sdk-js <[email protected]>
  • Loading branch information
shibeshduw and cloud-sdk-js authored Oct 18, 2024
1 parent 4715763 commit 65c50fa
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 96 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ jobs:
fi
done
wget -qO- -S --content-on-error localhost:8080
- name: 'Cleanup Deployments'
run: pnpm e2e-tests cleanup-deployments
- name: 'Execute E2E Tests'
run: pnpm test:e2e
- name: 'Slack Notification'
Expand Down
1 change: 1 addition & 0 deletions packages/ai-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ To ensure compatibility and manage updates effectively, we strongly recommend us
## Usage

The examples below demonstrate the usage of the most commonly used APIs in SAP AI Core.
In addition to the examples below, you can find more **sample code** [here](https://github.com/SAP/ai-sdk-js/blob/main/sample-code/src/ai-api).

### Create an Artifact

Expand Down
9 changes: 6 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 0 additions & 15 deletions sample-code/src/ai-api.ts

This file was deleted.

108 changes: 108 additions & 0 deletions sample-code/src/ai-api/deployment-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { DeploymentApi } from '@sap-ai-sdk/ai-api';
import type {
AiDeploymentBulkModificationResponse,
AiDeploymentCreationResponse,
AiDeploymentDeletionResponse,
AiDeploymentList,
AiDeploymentModificationRequestList,
AiDeploymentStatus
} from '@sap-ai-sdk/ai-api';

/**
* Get all deployments filtered by status.
* @param resourceGroup - AI-Resource-Group where the resources are available.
* @param status - Optional query parameter to filter deployments by status.
* @returns List of deployments.
*/
export async function getDeployments(
resourceGroup: string,
status?: AiDeploymentStatus
): Promise<AiDeploymentList> {
// check for optional query parameters.
const queryParams = status ? { status } : {};
return DeploymentApi.deploymentQuery(queryParams, {
'AI-Resource-Group': resourceGroup
}).execute();
}

/**
* Create a deployment using the configuration specified by configurationId.
* @param configurationId - ID of the configuration to be used.
* @param resourceGroup - AI-Resource-Group where the resources are available.
* @returns Deployment creation response with 'targetStatus': 'RUNNING'.
*/
export async function createDeployment(
configurationId: string,
resourceGroup: string
): Promise<AiDeploymentCreationResponse> {
return DeploymentApi.deploymentCreate(
{ configurationId },
{ 'AI-Resource-Group': resourceGroup }
).execute();
}

/**
* Stop all deployments with the specific configuration ID.
* Only deployments with 'status': 'RUNNING' can be stopped.
* @param configurationId - ID of the configuration to be used.
* @param resourceGroup - AI-Resource-Group where the resources are available.
* @returns Deployment modification response list with 'targetStatus': 'STOPPED'.
*/
export async function stopDeployments(
configurationId: string,
resourceGroup: string
): Promise<AiDeploymentBulkModificationResponse> {
// 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();
}

/**
* 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 list with 'targetStatus': 'DELETED'.
*/
export async function deleteDeployments(
resourceGroup: string
): Promise<AiDeploymentDeletionResponse[]> {
// 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()
)
);
}
30 changes: 30 additions & 0 deletions sample-code/src/ai-api/scenario-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ScenarioApi } from '@sap-ai-sdk/ai-api';
import type { AiScenarioList, AiModelList } from '@sap-ai-sdk/ai-api';

/**
* Get all scenarios.
* @param resourceGroup - AI-Resource-Group where the resources are available.
* @returns All scenarios.
*/
export async function getScenarios(
resourceGroup: string
): Promise<AiScenarioList> {
return ScenarioApi.scenarioQuery({
'AI-Resource-Group': resourceGroup
}).execute();
}

/**
* Retrieve information about all models available in LLM global scenario.
* @param scenarioId - ID of the global scenario.
* @param resourceGroup - AI-Resource-Group where the resources are available.
* @returns All models in given scenario.
*/
export async function getModelsInScenario(
scenarioId: string,
resourceGroup: string
): Promise<AiModelList> {
return ScenarioApi.modelsGet(scenarioId, {
'AI-Resource-Group': resourceGroup
}).execute();
}
12 changes: 12 additions & 0 deletions sample-code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ export {
invokeChain,
invokeRagChain
} from './langchain-azure-openai.js';
export {
getDeployments,
createDeployment,
stopDeployments,
deleteDeployments
// eslint-disable-next-line import/no-internal-modules
} from './ai-api/deployment-api.js';
export {
getScenarios,
getModelsInScenario
// eslint-disable-next-line import/no-internal-modules
} from './ai-api/scenario-api.js';
89 changes: 84 additions & 5 deletions sample-code/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,24 @@ import {
orchestrationOutputFiltering,
orchestrationRequestConfig
} from './orchestration.js';
import { getDeployments } from './ai-api.js';
import {
getDeployments,
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';
import {
invokeChain,
invokeRagChain,
invoke
} from './langchain-azure-openai.js';
import type { AiApiError, AiDeploymentStatus } from '@sap-ai-sdk/ai-api';
import type { OrchestrationResponse } from '@sap-ai-sdk/orchestration';

const app = express();
Expand Down Expand Up @@ -87,14 +99,81 @@ 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());
res.send(
await getDeployments('default', req.query.status as AiDeploymentStatus)
);
} catch (error: any) {
console.error(error);
const apiError = error.response.data.error as AiApiError;
res
.status(500)
.send('Yikes, vibes are off apparently 😬 -> ' + error.message);
.status(error.response.status)
.send('Yikes, vibes are off apparently 😬 -> ' + apiError.message);
}
});

app.post('/ai-api/deployment/create', express.json(), async (req, res) => {
try {
res.send(await createDeployment(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.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) {
console.error(error);
const apiError = error.response.data.error as AiApiError;
res
.status(error.response.status)
.send('Yikes, vibes are off apparently 😬 -> ' + apiError.message);
}
});

Expand Down
2 changes: 1 addition & 1 deletion tests/e2e-tests/jest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export default {
globalSetup: undefined,
globalTeardown: undefined,
displayName: 'e2e-tests',
testTimeout: 30000,
testTimeout: 45000,
};
4 changes: 3 additions & 1 deletion tests/e2e-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
"dependencies": {
"@sap-ai-sdk/ai-api": "workspace:^",
"@sap-ai-sdk/orchestration": "workspace:^",
"@sap-ai-sdk/sample-code": "workspace:^"
"@sap-ai-sdk/sample-code": "workspace:^",
"@sap-cloud-sdk/util": "^3.21.0"
},
"scripts": {
"compile": "tsc",
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
"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"
},
Expand Down
Loading

0 comments on commit 65c50fa

Please sign in to comment.