From 0fbe24e690c0b0aefbca43ceecfe17952d2a2dd5 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 15 Oct 2024 09:45:59 -0600 Subject: [PATCH 01/48] Remove "readable" values from being parsed out in model create review screen --- .../model-management/create-model/ReviewModelChanges.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx b/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx index bdea71b8..ea40d285 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx @@ -35,7 +35,7 @@ export function ReviewModelChanges (props: ReviewModelChangesProps) : ReactEleme for (const key in json) { const value = json[key]; - output.push((
  • {_.startCase(key)}{_.isPlainObject(value) ? '' : `: ${_.startCase(value)}`}

  • )); + output.push((
  • {_.startCase(key)}{_.isPlainObject(value) ? '' : `: ${value}`}

  • )); if (_.isPlainObject(value)) { const recursiveJson = jsonToOutline(value); // recursively call From 8e327c84847d2133becc1f5e69c0526f7f8ac250 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Tue, 15 Oct 2024 15:53:10 -0400 Subject: [PATCH 02/48] Moved alerts for create/update into modal --- .../create-model/CreateModelModal.tsx | 43 +++++++++++++++---- .../create-model/ReviewModelChanges.tsx | 14 +++++- .../reducers/model-management.reducer.ts | 14 ++++++ 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx b/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx index ba366efb..b8127aa7 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx @@ -33,6 +33,7 @@ import { useNotificationService } from '../../../shared/util/hooks'; import { ReviewModelChanges } from './ReviewModelChanges'; import { ModifyMethod } from '../../../shared/validation/modify-method'; import { z } from 'zod'; +import { SerializedError } from '@reduxjs/toolkit'; export type CreateModelModalProps = { visible: boolean; @@ -86,11 +87,11 @@ function getDefaults ( schema: z.AnyZodObject | z.ZodEff export function CreateModelModal (props: CreateModelModalProps) : ReactElement { const [ createModelMutation, - { isSuccess: isCreateSuccess, isError: isCreateError, error: createError, isLoading: isCreating }, + { isSuccess: isCreateSuccess, isError: isCreateError, error: createError, isLoading: isCreating, reset: resetCreate }, ] = useCreateModelMutation(); const [ updateModelMutation, - { isSuccess: isUpdateSuccess, isError: isUpdateError, error: updateError, isLoading: isUpdating }, + { isSuccess: isUpdateSuccess, isError: isUpdateError, error: updateError, isLoading: isUpdating, reset: resetUpdate }, ] = useUpdateModelMutation(); const initialForm = { ...getDefaults(ModelRequestSchema), @@ -131,6 +132,8 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { }, activeStepIndex: 0, }, ModifyMethod.Set); + resetCreate(); + resetUpdate(); } /** @@ -179,9 +182,11 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { function handleSubmit () { delete toSubmit.lisaHostedModel; if (isValid && !props.isEdit && !_.isEmpty(changesDiff)) { + resetCreate(); createModelMutation(toSubmit); } else if (isValid && props.isEdit && !_.isEmpty(changesDiff)) { // pick only the values we care about + resetUpdate(); updateModelMutation(_.mapKeys(_.pick({...changesDiff, modelId: props.selectedItems[0].modelId}, [ 'modelId', 'streaming', @@ -226,11 +231,9 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { props.setVisible(false); props.setIsEdit(false); resetState(); - } else if (!isCreating && isCreateError) { - notificationService.generateNotification(`Error creating model: ${createError.data.message ?? createError.data}`, 'error'); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isCreateError, createError, isCreating, isCreateSuccess]); + }, [isCreating, isCreateSuccess]); useEffect(() => { if (!isUpdating && isUpdateSuccess) { @@ -239,11 +242,33 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { props.setIsEdit(false); props.setSelectedItems([]); resetState(); - } else if (!isUpdating && isUpdateError) { - notificationService.generateNotification(`Error updating model: ${updateError.data.message ?? updateError.data}`, 'error'); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isUpdateError, updateError, isUpdating, isUpdateSuccess]); + }, [isUpdating, isUpdateSuccess]); + + const normalizeError = (error: SerializedError | {status: string, data: any}): SerializedError | undefined => { + // type predicate to help discriminate between types + function isResponseError (responseError: SerializedError | T): responseError is T { + return (responseError as T)?.status !== undefined; + } + + if (error !== undefined) { + if (isResponseError(error)) { + return { + name: 'Model Error', + message: error.status + }; + } else if (error) { + return { + name: error?.name || 'Model Error', + message: error?.message + }; + } + } + + return undefined; + }; + const reviewError = normalizeError(isCreateError ? createError : isUpdateError ? updateError : undefined); const steps = [ { @@ -280,7 +305,7 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { title: `Review and ${props.isEdit ? 'Update' : 'Create'}`, description: `Review configuration ${props.isEdit ? 'changes' : ''} prior to submitting.`, content: ( - + ), onEdit: state.form.lisaHostedModel } diff --git a/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx b/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx index ea40d285..2bcfb8f7 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/ReviewModelChanges.tsx @@ -16,11 +16,13 @@ import React, { ReactElement } from 'react'; import _ from 'lodash'; -import { SpaceBetween, TextContent } from '@cloudscape-design/components'; +import { Alert, SpaceBetween, TextContent } from '@cloudscape-design/components'; import Container from '@cloudscape-design/components/container'; +import { SerializedError } from '@reduxjs/toolkit'; export type ReviewModelChangesProps = { - jsonDiff: object + jsonDiff: object, + error?: SerializedError }; export function ReviewModelChanges (props: ReviewModelChangesProps) : ReactElement { @@ -52,6 +54,14 @@ export function ReviewModelChanges (props: ReviewModelChangesProps) : ReactEleme {_.isEmpty(props.jsonDiff) ?

    No changes detected

    : jsonToOutline(props.jsonDiff)} + + { props?.error && + { props?.error?.message } + } ); } diff --git a/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts b/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts index 49a7a36c..cf8b292e 100644 --- a/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts @@ -42,6 +42,13 @@ export const modelManagementApi = createApi({ method: 'POST', data: modelRequest }), + transformErrorResponse: (baseQueryReturnValue) => { + // transform into SerializedError + return { + name: 'Create Model Error', + message: baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') + }; + }, invalidatesTags: ['models'], }), updateModel: builder.mutation({ @@ -50,6 +57,13 @@ export const modelManagementApi = createApi({ method: 'PUT', data: modelRequest }), + transformErrorResponse: (baseQueryReturnValue) => { + // transform into SerializedError + return { + name: 'Update Model Error', + message: baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') + }; + }, invalidatesTags: ['models'], }), }), From 2e024eb0c77d816a6f68f80f53440565bdb0e249 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Wed, 16 Oct 2024 12:10:21 -0400 Subject: [PATCH 03/48] change instance input to select --- lambda/models/lambda_functions.py | 8 ++++++++ .../create-model/BaseModelConfig.tsx | 19 ++++++++++++++++--- .../reducers/model-management.reducer.ts | 14 ++++++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/lambda/models/lambda_functions.py b/lambda/models/lambda_functions.py index e39d641d..65856294 100644 --- a/lambda/models/lambda_functions.py +++ b/lambda/models/lambda_functions.py @@ -17,6 +17,7 @@ from typing import Annotated, Union import boto3 +import botocore.session from fastapi import FastAPI, Path, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse @@ -36,6 +37,7 @@ from .exception import InvalidStateTransitionError, ModelAlreadyExistsError, ModelNotFoundError from .handler import CreateModelHandler, DeleteModelHandler, GetModelHandler, ListModelsHandler, UpdateModelHandler +sess = botocore.session.Session() app = FastAPI(redirect_slashes=False, lifespan="off", docs_url="/docs", openapi_url="/openapi.json") app.add_middleware(AWSAPIGatewayMiddleware) @@ -135,5 +137,11 @@ async def delete_model( return delete_handler(model_id=model_id) +@app.get(path="/metadata/instances") # type: ignore +async def get_instances() -> list[str]: + """Endpoint to list available instances in this region.""" + return list(sess.get_service_model("ec2").shape_for("InstanceType").enum) + + handler = Mangum(app, lifespan="off", api_gateway_base_path="/models") docs = Mangum(app, lifespan="off") diff --git a/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx index 2d80b425..54420b5b 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx @@ -22,12 +22,15 @@ import Toggle from '@cloudscape-design/components/toggle'; import Select from '@cloudscape-design/components/select'; import { IModelRequest, InferenceContainer, ModelType } from '../../../shared/model/model-management.model'; import { Grid, SpaceBetween } from '@cloudscape-design/components'; +import { useGetInstancesQuery } from '../../../shared/reducers/model-management.reducer'; export type BaseModelConfigCustomProps = { isEdit: boolean }; export function BaseModelConfig (props: FormProps & BaseModelConfigCustomProps) : ReactElement { + const {data: instances, isLoading: isLoadingInstances} = useGetInstancesQuery(); + return ( @@ -68,9 +71,19 @@ export function BaseModelConfig (props: FormProps & BaseModelConf /> - props.touchFields(['instanceType'])} onChange={({ detail }) => { - props.setFields({ 'instanceType': detail.value }); - }} disabled={props.isEdit} placeholder='g5.xlarge'/> + ({ + query: () => ({ + url: '/models/metadata/instances' + }) + }) }), }); -export const { useGetAllModelsQuery, useDeleteModelMutation, useCreateModelMutation, useUpdateModelMutation } = - modelManagementApi; +export const { + useGetAllModelsQuery, + useDeleteModelMutation, + useCreateModelMutation, + useUpdateModelMutation, + useGetInstancesQuery +} = modelManagementApi; From 84d8f11efa95a8f40df3d00fd0eb8b2085f152b1 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Thu, 17 Oct 2024 18:01:41 +0000 Subject: [PATCH 04/48] unified error handling for model create/update form --- lambda/models/lambda_functions.py | 10 ++++++++++ .../src/shared/reducers/model-management.reducer.ts | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lambda/models/lambda_functions.py b/lambda/models/lambda_functions.py index 65856294..06aa0dce 100644 --- a/lambda/models/lambda_functions.py +++ b/lambda/models/lambda_functions.py @@ -19,6 +19,8 @@ import boto3 import botocore.session from fastapi import FastAPI, Path, Request +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from mangum import Mangum @@ -63,6 +65,14 @@ async def model_not_found_handler(request: Request, exc: ModelNotFoundError) -> return JSONResponse(status_code=404, content={"message": str(exc)}) +@app.exception_handler(RequestValidationError) # type: ignore +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Handle exception when request fails validation and and translate to a 422 error.""" + return JSONResponse( + status_code=422, content={"detail": jsonable_encoder(exc.errors()), "type": "RequestValidationError"} + ) + + @app.exception_handler(InvalidStateTransitionError) # type: ignore @app.exception_handler(ModelAlreadyExistsError) # type: ignore @app.exception_handler(ValueError) # type: ignore diff --git a/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts b/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts index 6e1d223d..994eb4c4 100644 --- a/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/model-management.reducer.ts @@ -46,7 +46,7 @@ export const modelManagementApi = createApi({ // transform into SerializedError return { name: 'Create Model Error', - message: baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') + message: baseQueryReturnValue.data?.type === 'RequestValidationError' ? baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') : baseQueryReturnValue.data.message }; }, invalidatesTags: ['models'], @@ -61,7 +61,7 @@ export const modelManagementApi = createApi({ // transform into SerializedError return { name: 'Update Model Error', - message: baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') + message: baseQueryReturnValue.data?.type === 'RequestValidationError' ? baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') : baseQueryReturnValue.data.message }; }, invalidatesTags: ['models'], From 14f02d4dbd4ad2d793bf33b8e35e0f378f260156 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 18 Oct 2024 10:32:52 -0600 Subject: [PATCH 05/48] Updating CDK nag to be enabled on the stacks vs app --- bin/lisa.ts | 6 ------ lib/stages.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bin/lisa.ts b/bin/lisa.ts index 94e6385a..b7763272 100644 --- a/bin/lisa.ts +++ b/bin/lisa.ts @@ -21,8 +21,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as cdk from 'aws-cdk-lib'; -import { Aspects } from 'aws-cdk-lib'; -import { AwsSolutionsChecks } from 'cdk-nag'; import * as yaml from 'js-yaml'; import { Config, ConfigFile, ConfigSchema } from '../lib/schema'; @@ -78,10 +76,6 @@ const env: cdk.Environment = { // Application const app = new cdk.App(); -// Run CDK-nag on app if specified -if (config.runCdkNag) { - Aspects.of(app).add(new AwsSolutionsChecks({ reports: true, verbose: true })); -} new LisaServeApplicationStage(app, config.deploymentStage, { env: env, diff --git a/lib/stages.ts b/lib/stages.ts index e92925cc..d0059924 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -28,6 +28,7 @@ import { Tags, } from 'aws-cdk-lib'; import { Construct } from 'constructs'; +import { AwsSolutionsChecks, NIST80053R5Checks } from 'cdk-nag'; import { LisaChatApplicationStack } from './chat'; import { CoreStack, ARCHITECTURE } from './core'; @@ -238,6 +239,14 @@ export class LisaServeApplicationStage extends Stage { }); } + // Run CDK-nag on app if specified + if (config.runCdkNag) { + stacks.forEach((lisaStack) => { + Aspects.of(lisaStack).add(new AwsSolutionsChecks({ reports: true, verbose: true })); + Aspects.of(lisaStack).add(new NIST80053R5Checks({ reports: true, verbose: true })); + }); + } + // Enforce updates to EC2 launch templates Aspects.of(this).add(new UpdateLaunchTemplateMetadataOptions()); } From 8a6f79f3eddea4e06934672e7d81c4005c5ec563 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Mon, 21 Oct 2024 11:02:06 -0600 Subject: [PATCH 06/48] Dynamic block device volume size and entry point changes --- ecs_model_deployer/src/lib/ecsCluster.ts | 2 +- ecs_model_deployer/src/lib/schema.ts | 1 + lambda/models/domain_objects.py | 3 +++ lib/api-base/ecsCluster.ts | 2 +- lib/schema.ts | 1 + .../ecs-model/textgen/tgi/src/entrypoint.sh | 20 ++++++++++++++++++- .../create-model/AutoScalingConfig.tsx | 8 ++++++++ .../shared/model/model-management.model.ts | 2 ++ 8 files changed, 36 insertions(+), 3 deletions(-) diff --git a/ecs_model_deployer/src/lib/ecsCluster.ts b/ecs_model_deployer/src/lib/ecsCluster.ts index f8316aef..640e0907 100644 --- a/ecs_model_deployer/src/lib/ecsCluster.ts +++ b/ecs_model_deployer/src/lib/ecsCluster.ts @@ -102,7 +102,7 @@ export class ECSCluster extends Construct { blockDevices: [ { deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(30, { + volume: BlockDeviceVolume.ebs(ecsConfig.autoScalingConfig.blockDeviceVolumeSize, { encrypted: true, }), }, diff --git a/ecs_model_deployer/src/lib/schema.ts b/ecs_model_deployer/src/lib/schema.ts index 06b4f972..f638f02a 100644 --- a/ecs_model_deployer/src/lib/schema.ts +++ b/ecs_model_deployer/src/lib/schema.ts @@ -417,6 +417,7 @@ const MetricConfigSchema = z.object({ * @property {MetricConfig} metricConfig - Metric configuration for auto scaling. */ const AutoScalingConfigSchema = z.object({ + blockDeviceVolumeSize: z.number().min(30).default(30), minCapacity: z.number().min(1).default(1), maxCapacity: z.number().min(1).default(2), defaultInstanceWarmup: z.number().default(180), diff --git a/lambda/models/domain_objects.py b/lambda/models/domain_objects.py index 46c29027..dfa7143e 100644 --- a/lambda/models/domain_objects.py +++ b/lambda/models/domain_objects.py @@ -91,6 +91,7 @@ class LoadBalancerConfig(BaseModel): class AutoScalingConfig(BaseModel): """Autoscaling configuration upon model creation.""" + blockDeviceVolumeSize: Optional[NonNegativeInt] = 30 minCapacity: NonNegativeInt maxCapacity: NonNegativeInt cooldown: PositiveInt @@ -102,6 +103,8 @@ def validate_auto_scaling_config(self) -> Self: """Validate autoScalingConfig values.""" if self.minCapacity > self.maxCapacity: raise ValueError("minCapacity must be less than or equal to the maxCapacity.") + if self.blockDeviceVolumeSize is not None and self.blockDeviceVolumeSize < 30: + raise ValueError("blockDeviceVolumeSize must be greater than or equal to 30.") return self diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 69bbcf7c..7c89636a 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -102,7 +102,7 @@ export class ECSCluster extends Construct { blockDevices: [ { deviceName: '/dev/xvda', - volume: BlockDeviceVolume.ebs(30, { + volume: BlockDeviceVolume.ebs(ecsConfig.autoScalingConfig.blockDeviceVolumeSize, { encrypted: true, }), }, diff --git a/lib/schema.ts b/lib/schema.ts index 40178c44..dc25ed5f 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -440,6 +440,7 @@ const MetricConfigSchema = z.object({ * @property {MetricConfig} metricConfig - Metric configuration for auto scaling. */ const AutoScalingConfigSchema = z.object({ + blockDeviceVolumeSize: z.number().min(30).default(30), minCapacity: z.number().min(1).default(1), maxCapacity: z.number().min(1).default(2), defaultInstanceWarmup: z.number().default(180), diff --git a/lib/serve/ecs-model/textgen/tgi/src/entrypoint.sh b/lib/serve/ecs-model/textgen/tgi/src/entrypoint.sh index be6270fc..9486e10f 100644 --- a/lib/serve/ecs-model/textgen/tgi/src/entrypoint.sh +++ b/lib/serve/ecs-model/textgen/tgi/src/entrypoint.sh @@ -27,11 +27,29 @@ echo "Setting environment variables" export MAX_CONCURRENT_REQUESTS="${MAX_CONCURRENT_REQUESTS}" export MAX_INPUT_LENGTH="${MAX_INPUT_LENGTH}" export MAX_TOTAL_TOKENS="${MAX_TOTAL_TOKENS}" + +startArgs=() + if [[ -n "${QUANTIZE}" ]]; then export QUANTIZE="${QUANTIZE}" + startArgs+=('--quantize' "${QUANTIZE}") +fi +# Check if CUDA_VISIBLE_DEVICES is set, otherwise set it to use GPU 0 +if [[ -z "${CUDA_VISIBLE_DEVICES}" ]]; then + export CUDA_VISIBLE_DEVICES="0" +fi +# Check if number of shards is set, otherwise set it to use 1 +if [[ -z "${NUM_SHARD}" ]]; then + export NUM_SHARD="${NUM_SHARD:-1}" fi echo "$(env)" +startArgs+=('--model-id' "${LOCAL_MODEL_PATH}") +startArgs+=('--port' '8080') +startArgs+=('--num-shard' "${NUM_SHARD}") +startArgs+=('--json-output') + # Start the webserver echo "Starting TGI" -text-generation-launcher --model-id $LOCAL_MODEL_PATH --port 8080 --json-output +CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES} \ +text-generation-launcher "${startArgs[@]}" diff --git a/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx index c8968cd3..cfcc4f3f 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/AutoScalingConfig.tsx @@ -35,6 +35,14 @@ export function AutoScalingConfig (props: AutoScalingConfigProps) : ReactElement
    Auto Scaling Capacity
    } > + + + props.touchFields(['autoScalingConfig.blockDeviceVolumeSize'])} disabled={props.isEdit} onChange={({ detail }) => { + props.setFields({ 'autoScalingConfig.blockDeviceVolumeSize': Number(detail.value) }); + }}/> + GBs + + props.touchFields(['autoScalingConfig.minCapacity'])} onChange={({ detail }) => { diff --git a/lib/user-interface/react/src/shared/model/model-management.model.ts b/lib/user-interface/react/src/shared/model/model-management.model.ts index 4b52969e..0721681c 100644 --- a/lib/user-interface/react/src/shared/model/model-management.model.ts +++ b/lib/user-interface/react/src/shared/model/model-management.model.ts @@ -72,6 +72,7 @@ export type ILoadBalancerConfig = { }; export type IAutoScalingConfig = { + blockDeviceVolumeSize: number; minCapacity: number; maxCapacity: number; desiredCapacity?: number; @@ -161,6 +162,7 @@ export const loadBalancerConfigSchema = z.object({ }); export const autoScalingConfigSchema = z.object({ + blockDeviceVolumeSize: z.number().min(30).default(30), minCapacity: z.number().min(1).default(1), maxCapacity: z.number().min(1).default(1), desiredCapacity: z.number().optional(), From 1f0f393f497a6ad5f465fc382116761a258aa05f Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Mon, 21 Oct 2024 11:56:26 -0600 Subject: [PATCH 07/48] Updating lead email --- .github/workflows/code.hotfix.branch.yml | 2 +- .github/workflows/code.merge.main-to-develop.yml | 2 +- .github/workflows/code.release.branch.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code.hotfix.branch.yml b/.github/workflows/code.hotfix.branch.yml index 34df3206..6f8bede9 100644 --- a/.github/workflows/code.hotfix.branch.yml +++ b/.github/workflows/code.hotfix.branch.yml @@ -24,7 +24,7 @@ jobs: ref: refs/tags/${{ github.event.inputs.source_tag }} - name: Create Hotfix Branch and Update Version run: | - git config --global user.email "petermul@amazon.com" + git config --global user.email "evmann@amazon.com" git config --global user.name "github_actions_lisa" SRC_TAG=${{ github.event.inputs.source_tag }} DST_TAG=${{ github.event.inputs.dest_tag }} diff --git a/.github/workflows/code.merge.main-to-develop.yml b/.github/workflows/code.merge.main-to-develop.yml index d5a47b27..0a18b259 100644 --- a/.github/workflows/code.merge.main-to-develop.yml +++ b/.github/workflows/code.merge.main-to-develop.yml @@ -18,7 +18,7 @@ jobs: ssh-key: ${{ secrets.DEPLOYMENT_SSH_KEY }} - name: merge main into develop run: | - git config --global user.email "petermul@amazon.com" + git config --global user.email "evmann@amazon.com" git config --global user.name "github_actions_lisa" git fetch --unshallow git checkout develop diff --git a/.github/workflows/code.release.branch.yml b/.github/workflows/code.release.branch.yml index 4b8e07b1..cbe3900c 100644 --- a/.github/workflows/code.release.branch.yml +++ b/.github/workflows/code.release.branch.yml @@ -21,7 +21,7 @@ jobs: ref: develop - name: Create Release Branch and Update Version run: | - git config --global user.email "petermul@amazon.com" + git config --global user.email "evmann@amazon.com" git config --global user.name "github_actions_lisa" RELEASE_TAG=${{ github.event.inputs.release_tag }} git checkout -b release/${{ github.event.inputs.release_tag }} From cd1e294968e9da2bd9e35844ff70f3981bbc5fb9 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 22 Oct 2024 10:23:10 -0600 Subject: [PATCH 08/48] Allow users to specify if they want private APIGW endpoints or not in the config --- example_config.yaml | 1 + lib/core/api_base.ts | 4 ++-- lib/schema.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example_config.yaml b/example_config.yaml index 8ac9dd02..f8d21dd2 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -37,6 +37,7 @@ dev: deployRag: true deployChat: true deployUi: true + privateEndpoints: false lambdaConfig: pythonRuntime: PYTHON_3_10 logLevel: DEBUG diff --git a/lib/core/api_base.ts b/lib/core/api_base.ts index c8de1bc0..65aebeef 100644 --- a/lib/core/api_base.ts +++ b/lib/core/api_base.ts @@ -15,7 +15,7 @@ */ import { Stack, StackProps } from 'aws-cdk-lib'; -import { Cors, EndpointType, Authorizer, RestApi, StageOptions } from 'aws-cdk-lib/aws-apigateway'; +import { Authorizer, Cors, EndpointType, RestApi, StageOptions } from 'aws-cdk-lib/aws-apigateway'; import { IVpc } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; @@ -47,7 +47,7 @@ export class LisaApiBaseStack extends Stack { const restApi = new RestApi(this, `${id}-RestApi`, { description: 'Base API Gateway for LISA.', - endpointConfiguration: { types: [EndpointType.REGIONAL] }, + endpointConfiguration: { types: [config.privateEndpoints ? EndpointType.PRIVATE : EndpointType.REGIONAL] }, deploy: true, deployOptions, defaultCorsPreflightOptions: { diff --git a/lib/schema.ts b/lib/schema.ts index dc25ed5f..2cbdf801 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -836,6 +836,7 @@ const RawConfigSchema = z deploymentStage: z.string(), removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]).transform((value) => REMOVAL_POLICIES[value]), runCdkNag: z.boolean().default(false), + privateEndpoints: z.boolean().optional().default(false), s3BucketModels: z.string(), mountS3DebUrl: z.string().optional(), accountNumbersEcr: z From f3d3ce48ac4423d057118edf5c990afd1ba672f7 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Wed, 23 Oct 2024 12:11:02 -0600 Subject: [PATCH 09/48] Enabling headless deployment in make file --- Makefile | 98 +++++++++++++++----------------------------------------- 1 file changed, 26 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index 201aa7bd..d66f1e80 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ ################################################################################# PROJECT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +HEADLESS = false # Arguments defined through command line or config.yaml @@ -239,90 +240,43 @@ listStacks: buildEcsDeployer: @cd ./ecs_model_deployer && npm install && npm run build +define print_config + @printf "\n \ + DEPLOYING $(STACK) STACK APP INFRASTRUCTURE \n \ + -----------------------------------\n \ + Account Number $(ACCOUNT_NUMBER)\n \ + Region $(REGION)\n \ + App Name $(APP_NAME)\n \ + Deployment Stage $(DEPLOYMENT_STAGE)\n \ + Deployment Name $(DEPLOYMENT_NAME)" + @if [ -n "$(PROFILE)" ]; then \ + printf "\n Deployment Profile $(PROFILE)"; \ + fi + @printf "\n-----------------------------------\n" +endef + ## Deploy all infrastructure deploy: dockerCheck dockerLogin cleanMisc modelCheck buildEcsDeployer -ifdef PROFILE - @printf "\n \ - DEPLOYING $(STACK) STACK APP INFRASTRUCTURE \n \ - -----------------------------------\n \ - Deployment Profile ${PROFILE}\n \ - Account Number ${ACCOUNT_NUMBER}\n \ - Region ${REGION}\n \ - App Name ${APP_NAME}\n \ - Deployment Stage ${DEPLOYMENT_STAGE}\n \ - Deployment Name ${DEPLOYMENT_NAME}\n \ - -----------------------------------\n \ - Is the configuration correct? [y/N] "\ - && read confirm_config &&\ - if \ - [ $${confirm_config:-'N'} = 'y' ]; \ - then \ - npx cdk deploy ${STACK} \ - --profile ${PROFILE} \ - -c ${ENV}='$(shell echo '${${ENV}}')'; \ - fi; + $(call print_config) +ifneq (,$(findstring true, $(HEADLESS))) + npx cdk deploy ${STACK} $(if $(PROFILE),--profile ${PROFILE}) --require-approval never -c ${ENV}='$(shell echo '${${ENV}}')'; else - @printf "\n \ - DEPLOYING $(STACK) STACK APP INFRASTRUCTURE \n \ - -----------------------------------\n \ - Account Number ${ACCOUNT_NUMBER}\n \ - Region ${REGION}\n \ - App Name ${APP_NAME}\n \ - Deployment Stage ${DEPLOYMENT_STAGE}\n \ - Deployment Name ${DEPLOYMENT_NAME}\n \ - -----------------------------------\n \ - Is the configuration correct? [y/N] "\ + @printf "Is the configuration correct? [y/N] "\ && read confirm_config &&\ - if \ - [ $${confirm_config:-'N'} = 'y' ]; \ - then \ - npx cdk deploy ${STACK} \ - -c ${ENV}='$(shell echo '${${ENV}}')'; \ + if [ $${confirm_config:-'N'} = 'y' ]; then \ + npx cdk deploy ${STACK} $(if $(PROFILE),--profile ${PROFILE}) -c ${ENV}='$(shell echo '${${ENV}}')'; \ fi; endif ## Tear down all infrastructure destroy: cleanMisc -ifdef PROFILE - @printf "\n \ - DESTROYING $(STACK) STACK APP INFRASTRUCTURE \n \ - -----------------------------------\n \ - Deployment Profile ${PROFILE}\n \ - Account Number ${ACCOUNT_NUMBER}\n \ - Region ${REGION}\n \ - App Name ${APP_NAME}\n \ - Deployment Stage ${DEPLOYMENT_STAGE}\n \ - Deployment Name ${DEPLOYMENT_NAME}\n \ - -----------------------------------\n \ - Is the configuration correct? [y/N] "\ + $(call print_config) + @printf "Is the configuration correct? [y/N] "\ && read confirm_config &&\ - if \ - [ $${confirm_config:-'N'} = 'y' ]; \ - then \ - npx cdk destroy ${STACK} \ - --force \ - --profile ${PROFILE}; \ - fi; -else - @printf "\n \ - DESTROYING $(STACK) STACK APP INFRASTRUCTURE \n \ - -----------------------------------\n \ - Account Number ${ACCOUNT_NUMBER}\n \ - Region ${REGION}\n \ - App Name ${APP_NAME}\n \ - Deployment Stage ${DEPLOYMENT_STAGE}\n \ - Deployment Name ${DEPLOYMENT_NAME}\n \ - -----------------------------------\n \ - Is the configuration correct? [y/N] "\ - && read confirm_config &&\ - if \ - [ $${confirm_config:-'N'} = 'y' ]; \ - then \ - npx cdk destroy ${STACK} \ - --force; \ + if [ $${confirm_config:-'N'} = 'y' ]; then \ + npx cdk destroy ${STACK} --force $(if $(PROFILE),--profile ${PROFILE}); \ fi; -endif ################################################################################# From 0158374308683d4e1e4166002938ba3569773f02 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 24 Oct 2024 11:29:23 -0600 Subject: [PATCH 10/48] private vpc and subnets --- ecs_model_deployer/src/lib/ecs-model.ts | 6 +++-- ecs_model_deployer/src/lib/ecsCluster.ts | 7 +++-- .../src/lib/lisa_model_stack.ts | 13 +++++++-- ecs_model_deployer/src/lib/schema.ts | 1 + example_config.yaml | 2 ++ lib/api-base/authorizer.ts | 9 ++++--- lib/api-base/ecsCluster.ts | 11 +++++--- lib/api-base/fastApiContainer.ts | 8 +++--- lib/api-base/utils.ts | 8 +++--- lib/chat/api/session.ts | 6 +++-- lib/chat/index.ts | 5 ++-- lib/core/api_base.ts | 4 +-- lib/models/docker-image-builder.ts | 6 ++++- lib/models/ecs-model-deployer.ts | 12 ++++++--- lib/models/model-api.ts | 17 ++++++------ lib/models/state-machine/create-model.ts | 26 +++++++++++------- lib/models/state-machine/delete-model.ts | 20 +++++++++----- lib/models/state-machine/update-model.ts | 14 ++++++---- lib/networking/vpc/index.ts | 27 +++++++++++++++++++ lib/rag/api/repository.ts | 5 ++-- lib/rag/index.ts | 7 +++-- lib/schema.ts | 1 + lib/serve/index.ts | 7 +++-- lib/stages.ts | 4 +-- test/cdk/stacks/chat.test.ts | 4 +-- test/cdk/stacks/core-api-base.test.ts | 2 +- test/cdk/stacks/core-api-deploy.test.ts | 2 +- test/cdk/stacks/ui.test.ts | 2 +- 28 files changed, 164 insertions(+), 72 deletions(-) diff --git a/ecs_model_deployer/src/lib/ecs-model.ts b/ecs_model_deployer/src/lib/ecs-model.ts index 5710767e..66016cf6 100644 --- a/ecs_model_deployer/src/lib/ecs-model.ts +++ b/ecs_model_deployer/src/lib/ecs-model.ts @@ -15,7 +15,7 @@ */ // ECS Model Construct. -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup, IVpc, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; import { AmiHardwareType } from 'aws-cdk-lib/aws-ecs'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; @@ -40,6 +40,7 @@ type ECSModelProps = { modelConfig: ModelConfig; securityGroup: ISecurityGroup; vpc: IVpc; + subnetSelection?: SubnetSelection; } & BaseProps; /** @@ -56,7 +57,7 @@ export class EcsModel extends Construct { */ constructor (scope: Construct, id: string, props: ECSModelProps) { super(scope, id); - const { config, modelConfig, securityGroup, vpc } = props; + const { config, modelConfig, securityGroup, vpc, subnetSelection } = props; const modelCluster = new ECSCluster(scope, `${id}-ECC`, { config, @@ -74,6 +75,7 @@ export class EcsModel extends Construct { }, securityGroup, vpc, + subnetSelection }); // Single bucket for all models diff --git a/ecs_model_deployer/src/lib/ecsCluster.ts b/ecs_model_deployer/src/lib/ecsCluster.ts index 640e0907..9218d374 100644 --- a/ecs_model_deployer/src/lib/ecsCluster.ts +++ b/ecs_model_deployer/src/lib/ecsCluster.ts @@ -18,7 +18,7 @@ import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; import { BlockDeviceVolume, GroupMetrics, Monitoring } from 'aws-cdk-lib/aws-autoscaling'; import { Metric, Stats } from 'aws-cdk-lib/aws-cloudwatch'; -import { InstanceType, ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { InstanceType, ISecurityGroup, IVpc, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { AmiHardwareType, @@ -57,6 +57,7 @@ type ECSClusterProps = { ecsConfig: ECSConfig; securityGroup: ISecurityGroup; vpc: IVpc; + subnetSelection?: SubnetSelection; } & BaseProps; /** @@ -79,7 +80,7 @@ export class ECSCluster extends Construct { */ constructor (scope: Construct, id: string, props: ECSClusterProps) { super(scope, id); - const { config, vpc, securityGroup, ecsConfig } = props; + const { config, vpc, securityGroup, ecsConfig, subnetSelection } = props; // Create ECS cluster const cluster = new Cluster(this, createCdkId([ecsConfig.identifier, 'Cl']), { @@ -90,6 +91,7 @@ export class ECSCluster extends Construct { // Create auto scaling group const autoScalingGroup = cluster.addCapacity(createCdkId([ecsConfig.identifier, 'ASG']), { + vpcSubnets: subnetSelection, instanceType: new InstanceType(ecsConfig.instanceType), machineImage: EcsOptimizedImage.amazonLinux2(ecsConfig.amiHardwareType), minCapacity: ecsConfig.autoScalingConfig.minCapacity, @@ -285,6 +287,7 @@ export class ECSCluster extends Construct { dropInvalidHeaderFields: true, securityGroup, vpc, + vpcSubnets: subnetSelection, idleTimeout: Duration.seconds(600) }); diff --git a/ecs_model_deployer/src/lib/lisa_model_stack.ts b/ecs_model_deployer/src/lib/lisa_model_stack.ts index 4faa5acf..8bc5f962 100644 --- a/ecs_model_deployer/src/lib/lisa_model_stack.ts +++ b/ecs_model_deployer/src/lib/lisa_model_stack.ts @@ -16,7 +16,7 @@ import { Stack, StackProps } from 'aws-cdk-lib'; -import { Vpc, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { Vpc, SecurityGroup, Subnet, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; import { EcsModel } from './ecs-model'; @@ -38,13 +38,22 @@ export class LisaModelStack extends Stack { vpcId: props.vpcId }); + let subnetSelection: SubnetSelection | undefined; + + if (props.config.subnetIds && props.config.subnetIds.length > 0) { + subnetSelection = { + subnets: props.config.subnetIds?.map((subnet, index) => Subnet.fromSubnetId(this, index.toString(), subnet)) + }; + } + const securityGroup = SecurityGroup.fromLookupById(this, `${id}-sg`, props.securityGroupId); new EcsModel(this, `${id}-ecsModel`, { config: props.config, modelConfig: props.modelConfig, securityGroup: securityGroup, - vpc: vpc + vpc: vpc, + subnetSelection: subnetSelection }); } } diff --git a/ecs_model_deployer/src/lib/schema.ts b/ecs_model_deployer/src/lib/schema.ts index f638f02a..fda17ba9 100644 --- a/ecs_model_deployer/src/lib/schema.ts +++ b/ecs_model_deployer/src/lib/schema.ts @@ -618,6 +618,7 @@ const RawConfigSchema = z instanceProfilePrefix: z.string().optional(), }) .optional(), + subnetIds: z.array(z.string()).optional(), }) .refine((config) => (config.pypiConfig.indexUrl && config.region.includes('iso')) || !config.region.includes('iso'), { message: 'Must set PypiConfig if in an iso region', diff --git a/example_config.yaml b/example_config.yaml index f8d21dd2..e481b094 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -25,6 +25,8 @@ dev: # text: 'LISA System' # backgroundColor: orange # fontColor: black + # vpcId: vpc-0123456789abcdef, + # subnetIds: [subnet-fedcba9876543210, subnet-0987654321fedcba], s3BucketModels: hf-models-gaiic # aws partition mountS3 package location mountS3DebUrl: https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.deb diff --git a/lib/api-base/authorizer.ts b/lib/api-base/authorizer.ts index 358b8b6c..57661b31 100644 --- a/lib/api-base/authorizer.ts +++ b/lib/api-base/authorizer.ts @@ -16,7 +16,7 @@ import * as cdk from 'aws-cdk-lib'; import { RequestAuthorizer, IdentitySource } from 'aws-cdk-lib/aws-apigateway'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { Code, Function, LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -25,6 +25,7 @@ import { Construct } from 'constructs'; import { BaseProps } from '../schema'; import { createCdkId } from '../core/utils'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Vpc } from '../networking/vpc'; /** * Properties for RestApiGateway Construct. @@ -33,10 +34,11 @@ import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; * @property {Layer} authorizerLayer - Lambda layer for authorizer lambda. * @property {IRole} role - Execution role for lambdas * @property {ISecurityGroup[]} securityGroups - Security groups for Lambdas + * @property {Map} importedSubnets for Lambdas */ type AuthorizerProps = { role?: IRole; - vpc?: IVpc; + vpc?: Vpc; securityGroups?: ISecurityGroup[]; } & BaseProps; @@ -89,8 +91,9 @@ export class CustomAuthorizer extends Construct { MANAGEMENT_KEY_NAME: managementKeySecretNameStringParameter.stringValue }, role: role, - vpc: vpc, + vpc: vpc?.vpc, securityGroups: securityGroups, + vpcSubnets: vpc?.subnetSelection }); const managementKeySecret = Secret.fromSecretNameV2(this, createCdkId([id, 'managementKey']), managementKeySecretNameStringParameter.stringValue); diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 7c89636a..7a9b381d 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -18,7 +18,7 @@ import { Duration, RemovalPolicy } from 'aws-cdk-lib'; import { BlockDeviceVolume, GroupMetrics, Monitoring } from 'aws-cdk-lib/aws-autoscaling'; import { Metric, Stats } from 'aws-cdk-lib/aws-cloudwatch'; -import { InstanceType, IVpc, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { InstanceType, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { AmiHardwareType, @@ -45,6 +45,7 @@ import { Construct } from 'constructs'; import { createCdkId } from '../core/utils'; import { BaseProps, Ec2Metadata, EcsSourceType } from '../schema'; import { ECSConfig } from '../schema'; +import { Vpc } from '../networking/vpc'; /** * Properties for the ECSCluster Construct. @@ -56,7 +57,7 @@ import { ECSConfig } from '../schema'; type ECSClusterProps = { ecsConfig: ECSConfig; securityGroup: SecurityGroup; - vpc: IVpc; + vpc: Vpc; } & BaseProps; /** @@ -84,12 +85,13 @@ export class ECSCluster extends Construct { // Create ECS cluster const cluster = new Cluster(this, createCdkId(['Cl']), { clusterName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), - vpc: vpc, + vpc: vpc.vpc, containerInsights: !config.region.includes('iso'), }); // Create auto scaling group const autoScalingGroup = cluster.addCapacity(createCdkId(['ASG']), { + vpcSubnets: vpc.subnetSelection, instanceType: new InstanceType(ecsConfig.instanceType), machineImage: EcsOptimizedImage.amazonLinux2(ecsConfig.amiHardwareType), minCapacity: ecsConfig.autoScalingConfig.minCapacity, @@ -265,7 +267,8 @@ export class ECSCluster extends Construct { loadBalancerName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2).toLowerCase(), dropInvalidHeaderFields: true, securityGroup, - vpc, + vpc: vpc.vpc, + vpcSubnets: vpc.subnetSelection, idleTimeout: Duration.seconds(600) }); diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index d90f0e9d..1c79492b 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -16,7 +16,7 @@ import { CfnOutput } from 'aws-cdk-lib'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; -import { IVpc, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { SecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { AmiHardwareType, ContainerDefinition } from 'aws-cdk-lib/aws-ecs'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; @@ -24,6 +24,7 @@ import { dump as yamlDump } from 'js-yaml'; import { ECSCluster } from './ecsCluster'; import { BaseProps, Ec2Metadata, EcsSourceType, FastApiContainerConfig } from '../schema'; +import { Vpc } from '../networking/vpc'; // This is the amount of memory to buffer (or subtract off) from the total instance memory, if we don't include this, // the container can have a hard time finding available RAM resources to start and the tasks will fail deployment @@ -34,6 +35,7 @@ const CONTAINER_MEMORY_BUFFER = 1024 * 2; * * @property {IVpc} vpc - The virtual private cloud (VPC). * @property {SecurityGroup} securityGroups - The security groups of the application. + * @property {Map} importedSubnets for application. */ type FastApiContainerProps = { apiName: string; @@ -41,7 +43,7 @@ type FastApiContainerProps = { securityGroup: SecurityGroup; taskConfig: FastApiContainerConfig; tokenTable: ITable | undefined; - vpc: IVpc; + vpc: Vpc; } & BaseProps; /** @@ -113,7 +115,7 @@ export class FastApiContainer extends Construct { loadBalancerConfig: taskConfig.loadBalancerConfig, }, securityGroup, - vpc, + vpc }); if (tokenTable) { diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index 095ce633..1d0b6d6f 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -34,10 +34,11 @@ import { IRestApi, Cors, } from 'aws-cdk-lib/aws-apigateway'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { Code, Function, Runtime, ILayerVersion, IFunction, CfnPermission } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; +import { Vpc } from '../networking/vpc'; /** * Type representing python lambda function @@ -81,7 +82,7 @@ export function registerAPIEndpoint ( funcDef: PythonLambdaFunction, pythonRuntime: Runtime, role?: IRole, - vpc?: IVpc, + vpc?: Vpc, securityGroups?: ISecurityGroup[], ): IFunction { const functionId = `${ @@ -116,8 +117,9 @@ export function registerAPIEndpoint ( memorySize: 512, layers, role, - vpc, + vpc: vpc?.vpc, securityGroups, + vpcSubnets: vpc?.subnetSelection, }); } diff --git a/lib/chat/api/session.ts b/lib/chat/api/session.ts index 64d68df1..d80eccc8 100644 --- a/lib/chat/api/session.ts +++ b/lib/chat/api/session.ts @@ -16,7 +16,7 @@ import { IAuthorizer, RestApi } from 'aws-cdk-lib/aws-apigateway'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { Role } from 'aws-cdk-lib/aws-iam'; import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -25,6 +25,7 @@ import { Construct } from 'constructs'; import { PythonLambdaFunction, registerAPIEndpoint } from '../../api-base/utils'; import { BaseProps } from '../../schema'; import { createLambdaRole } from '../../core/utils'; +import { Vpc } from '../../networking/vpc'; /** * Properties for SessionApi Construct. @@ -35,13 +36,14 @@ import { createLambdaRole } from '../../core/utils'; * @property {IRole} lambdaExecutionRole - Execution role for lambdas * @property {IAuthorizer} authorizer - APIGW authorizer * @property {ISecurityGroup[]} securityGroups - Security groups for Lambdas + * @property {Map }importedSubnets for application. */ type SessionApiProps = { authorizer: IAuthorizer; restApiId: string; rootResourceId: string; securityGroups?: ISecurityGroup[]; - vpc?: IVpc; + vpc?: Vpc; } & BaseProps; /** diff --git a/lib/chat/index.ts b/lib/chat/index.ts index dfb8a11f..db3bbf82 100644 --- a/lib/chat/index.ts +++ b/lib/chat/index.ts @@ -17,18 +17,19 @@ // LisaChat Stack. import { Stack, StackProps } from 'aws-cdk-lib'; import { IAuthorizer } from 'aws-cdk-lib/aws-apigateway'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; import { SessionApi } from './api/session'; import { BaseProps } from '../schema'; +import { Vpc } from '../networking/vpc'; type CustomLisaChatStackProps = { authorizer: IAuthorizer; restApiId: string; rootResourceId: string; securityGroups?: ISecurityGroup[]; - vpc?: IVpc; + vpc?: Vpc; } & BaseProps; type LisaChatStackProps = CustomLisaChatStackProps & StackProps; diff --git a/lib/core/api_base.ts b/lib/core/api_base.ts index 65aebeef..18654357 100644 --- a/lib/core/api_base.ts +++ b/lib/core/api_base.ts @@ -16,14 +16,14 @@ import { Stack, StackProps } from 'aws-cdk-lib'; import { Authorizer, Cors, EndpointType, RestApi, StageOptions } from 'aws-cdk-lib/aws-apigateway'; -import { IVpc } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; import { CustomAuthorizer } from '../api-base/authorizer'; import { BaseProps } from '../schema'; +import { Vpc } from '../networking/vpc'; type LisaApiBaseStackProps = { - vpc: IVpc; + vpc: Vpc; } & BaseProps & StackProps; diff --git a/lib/models/docker-image-builder.ts b/lib/models/docker-image-builder.ts index 664843d6..f8190fe6 100644 --- a/lib/models/docker-image-builder.ts +++ b/lib/models/docker-image-builder.ts @@ -23,10 +23,12 @@ import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; import { createCdkId } from '../core/utils'; import { BaseProps } from '../schema'; +import { Vpc } from '../networking/vpc'; export type DockerImageBuilderProps = BaseProps & { ecrUri: string; mountS3DebUrl: string; + vpc?: Vpc; }; export class DockerImageBuilder extends Construct { @@ -126,7 +128,9 @@ export class DockerImageBuilder extends Construct { 'LISA_ECR_URI': props.ecrUri, 'LISA_INSTANCE_PROFILE': ec2InstanceProfile.instanceProfileArn, 'LISA_MOUNTS3_DEB_URL': props.mountS3DebUrl - } + }, + vpc: props.vpc?.subnetSelection ? props.vpc?.vpc : undefined, + vpcSubnets: props.vpc?.subnetSelection, }); } diff --git a/lib/models/ecs-model-deployer.ts b/lib/models/ecs-model-deployer.ts index 9c64949d..09c06174 100644 --- a/lib/models/ecs-model-deployer.ts +++ b/lib/models/ecs-model-deployer.ts @@ -21,11 +21,12 @@ import { Stack, Duration, Size } from 'aws-cdk-lib'; import { createCdkId } from '../core/utils'; import { BaseProps, Config } from '../schema'; +import { Vpc } from '../networking/vpc'; export type ECSModelDeployerProps = { - vpcId: string; securityGroupId: string; config: Config; + vpc: Vpc; } & BaseProps; export class ECSModelDeployer extends Construct { @@ -57,7 +58,8 @@ export class ECSModelDeployer extends Construct { 'removalPolicy': props.config.removalPolicy, 's3BucketModels': props.config.s3BucketModels, 'mountS3DebUrl': props.config.mountS3DebUrl, - 'permissionsBoundaryAspect': props.config.permissionsBoundaryAspect + 'permissionsBoundaryAspect': props.config.permissionsBoundaryAspect, + 'subnetIds': props.config.subnetIds }; const functionId = createCdkId([stackName, 'ecs_model_deployer']); @@ -69,10 +71,12 @@ export class ECSModelDeployer extends Construct { memorySize: 1024, role: role, environment: { - 'LISA_VPC_ID': props.vpcId, + 'LISA_VPC_ID': props.vpc?.vpc.vpcId, 'LISA_SECURITY_GROUP_ID': props.securityGroupId, 'LISA_CONFIG': JSON.stringify(stripped_config) - } + }, + vpc: props.vpc?.subnetSelection ? props.vpc?.vpc : undefined, + vpcSubnets: props.vpc?.subnetSelection, }); } } diff --git a/lib/models/model-api.ts b/lib/models/model-api.ts index e18afc5c..333228ce 100644 --- a/lib/models/model-api.ts +++ b/lib/models/model-api.ts @@ -114,14 +114,15 @@ export class ModelsApi extends Construct { const ecsModelDeployer = new ECSModelDeployer(this, 'ecs-model-deployer', { securityGroupId: vpc.securityGroups.ecsModelAlbSg.securityGroupId, - vpcId: vpc.vpc.vpcId, - config: config + config: config, + vpc: vpc }); const dockerImageBuilder = new DockerImageBuilder(this, 'docker-image-builder', { ecrUri: ecsModelBuildRepo.repositoryUri, mountS3DebUrl: config.mountS3DebUrl!, - config: config + config: config, + vpc }); const managementKeyName = StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/managementKeySecretName`); @@ -219,7 +220,7 @@ export class ModelsApi extends Construct { modelTable: modelTable, lambdaLayers: [commonLambdaLayer, fastapiLambdaLayer], role: stateMachinesLambdaRole, - vpc: vpc.vpc, + vpc: vpc, securityGroups: securityGroups, dockerImageBuilderFnArn: dockerImageBuilder.dockerImageBuilderFn.functionArn, ecsModelDeployerFnArn: ecsModelDeployer.ecsModelDeployerFn.functionArn, @@ -233,7 +234,7 @@ export class ModelsApi extends Construct { modelTable: modelTable, lambdaLayers: [commonLambdaLayer, fastapiLambdaLayer], role: stateMachinesLambdaRole, - vpc: vpc.vpc, + vpc: vpc, securityGroups: securityGroups, restApiContainerEndpointPs: lisaServeEndpointUrlPs, managementKeyName: managementKeyName, @@ -244,7 +245,7 @@ export class ModelsApi extends Construct { modelTable: modelTable, lambdaLayers: [commonLambdaLayer, fastapiLambdaLayer], role: stateMachinesLambdaRole, - vpc: vpc.vpc, + vpc: vpc, securityGroups: securityGroups, restApiContainerEndpointPs: lisaServeEndpointUrlPs, managementKeyName: managementKeyName, @@ -278,7 +279,7 @@ export class ModelsApi extends Construct { }, config.lambdaConfig.pythonRuntime, lambdaRole, - vpc.vpc, + vpc, securityGroups, ); lisaServeEndpointUrlPs.grantRead(lambdaFunction.role!); @@ -350,7 +351,7 @@ export class ModelsApi extends Construct { f, config.lambdaConfig.pythonRuntime, lambdaRole, - vpc.vpc, + vpc, securityGroups, ); }); diff --git a/lib/models/state-machine/create-model.ts b/lib/models/state-machine/create-model.ts index 946553f4..9596a4db 100644 --- a/lib/models/state-machine/create-model.ts +++ b/lib/models/state-machine/create-model.ts @@ -30,10 +30,11 @@ import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { Code, Function, ILayerVersion } from 'aws-cdk-lib/aws-lambda'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { LAMBDA_MEMORY, LAMBDA_TIMEOUT, OUTPUT_PATH, POLLING_TIMEOUT } from './constants'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Vpc } from '../../networking/vpc'; type CreateModelStateMachineProps = BaseProps & { modelTable: ITable, @@ -42,7 +43,7 @@ type CreateModelStateMachineProps = BaseProps & { ecsModelDeployerFnArn: string; ecsModelImageRepository: Repository; role?: IRole, - vpc?: IVpc, + vpc?: Vpc, securityGroups?: ISecurityGroup[]; restApiContainerEndpointPs: IStringParameter; managementKeyName: string @@ -79,7 +80,8 @@ export class CreateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -97,7 +99,8 @@ export class CreateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -113,7 +116,8 @@ export class CreateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -129,7 +133,8 @@ export class CreateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -151,7 +156,8 @@ export class CreateModelStateMachine extends Construct { timeout: Duration.minutes(8), memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -167,7 +173,8 @@ export class CreateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -189,7 +196,8 @@ export class CreateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, diff --git a/lib/models/state-machine/delete-model.ts b/lib/models/state-machine/delete-model.ts index 3016a08b..9d46ef38 100644 --- a/lib/models/state-machine/delete-model.ts +++ b/lib/models/state-machine/delete-model.ts @@ -28,16 +28,17 @@ import { import { Code, Function, ILayerVersion } from 'aws-cdk-lib/aws-lambda'; import { BaseProps } from '../../schema'; import { IRole } from 'aws-cdk-lib/aws-iam'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { LAMBDA_MEMORY, LAMBDA_TIMEOUT, OUTPUT_PATH, POLLING_TIMEOUT } from './constants'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Vpc } from '../../networking/vpc'; type DeleteModelStateMachineProps = BaseProps & { modelTable: ITable, lambdaLayers: ILayerVersion[], role?: IRole, - vpc?: IVpc, + vpc?: Vpc, securityGroups?: ISecurityGroup[]; restApiContainerEndpointPs: IStringParameter; managementKeyName: string; @@ -73,7 +74,8 @@ export class DeleteModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -89,7 +91,8 @@ export class DeleteModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -105,7 +108,8 @@ export class DeleteModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -121,7 +125,8 @@ export class DeleteModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -137,7 +142,8 @@ export class DeleteModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, diff --git a/lib/models/state-machine/update-model.ts b/lib/models/state-machine/update-model.ts index 87fc89a4..2bbb6fda 100644 --- a/lib/models/state-machine/update-model.ts +++ b/lib/models/state-machine/update-model.ts @@ -19,19 +19,20 @@ import { BaseProps } from '../../schema'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { Code, Function, ILayerVersion } from 'aws-cdk-lib/aws-lambda'; import { IRole } from 'aws-cdk-lib/aws-iam'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks'; import { LAMBDA_MEMORY, LAMBDA_TIMEOUT, OUTPUT_PATH, POLLING_TIMEOUT } from './constants'; import { Choice, Condition, DefinitionBody, StateMachine, Succeed, Wait, WaitTime } from 'aws-cdk-lib/aws-stepfunctions'; +import { Vpc } from '../../networking/vpc'; type UpdateModelStateMachineProps = BaseProps & { modelTable: ITable, lambdaLayers: ILayerVersion[], role?: IRole, - vpc?: IVpc, + vpc?: Vpc, securityGroups?: ISecurityGroup[]; restApiContainerEndpointPs: IStringParameter; managementKeyName: string; @@ -74,7 +75,8 @@ export class UpdateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -90,7 +92,8 @@ export class UpdateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, @@ -106,7 +109,8 @@ export class UpdateModelStateMachine extends Construct { timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, role: role, - vpc: vpc, + vpc: vpc?.vpc, + vpcSubnets: vpc?.subnetSelection, securityGroups: securityGroups, layers: lambdaLayers, environment: environment, diff --git a/lib/networking/vpc/index.ts b/lib/networking/vpc/index.ts index d9a767ad..414123d0 100644 --- a/lib/networking/vpc/index.ts +++ b/lib/networking/vpc/index.ts @@ -26,11 +26,13 @@ import { Port, SecurityGroup, SubnetType, + Subnet, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; import { createCdkId } from '../../core/utils'; import { SecurityGroups, BaseProps } from '../../schema'; +import { SubnetGroup } from 'aws-cdk-lib/aws-rds'; type VpcProps = {} & BaseProps; @@ -44,6 +46,12 @@ export class Vpc extends Construct { /** Security groups for application. */ public readonly securityGroups: SecurityGroups; + /** Created from deployment configured Subnets for application. */ + public readonly subnetGroup?: SubnetGroup; + + /** Imported Subnets for application. */ + public readonly subnetSelection?: SubnetSelection; + /** * @param {Construct} scope - The parent or owner of the construct. * @param {string} id - The unique identifier for the construct within its scope. @@ -54,9 +62,28 @@ export class Vpc extends Construct { let vpc: IVpc; if (config.vpcId) { + // Imports VPC for use by application if supplied, else creates a VPC. vpc = ec2Vpc.fromLookup(this, 'imported-vpc', { vpcId: config.vpcId, }); + + // Checks if SubnetIds are provided in the config, if so we import them for use. + // A VPC must be supplied if Subnets are being used. + if (config.subnetIds && config.subnetIds.length > 0) { + this.subnetSelection = { + subnets: props.config.subnetIds?.map((subnet, index) => Subnet.fromSubnetId(this, index.toString(), subnet)) + }; + + this.subnetGroup = new SubnetGroup( + this, + createCdkId([config.deploymentName, 'Imported-Subnets']), + { + vpc: this.vpc, + description: 'This SubnetGroup is made up of imported Subnets via the deployment config', + vpcSubnets: this.subnetSelection, + } + ); + } } else { // Create VPC vpc = new ec2Vpc(this, 'VPC', { diff --git a/lib/rag/api/repository.ts b/lib/rag/api/repository.ts index 3b1553e5..d0633058 100644 --- a/lib/rag/api/repository.ts +++ b/lib/rag/api/repository.ts @@ -16,13 +16,14 @@ import { Duration } from 'aws-cdk-lib'; import { IAuthorizer, RestApi } from 'aws-cdk-lib/aws-apigateway'; -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { ILayerVersion } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; import { PythonLambdaFunction, registerAPIEndpoint } from '../../api-base/utils'; import { BaseProps } from '../../schema'; +import { Vpc } from '../../networking/vpc'; /** * Properties for RepositoryAPI Construct. @@ -44,7 +45,7 @@ type RepositoryApiProps = { restApiId: string; rootResourceId: string; securityGroups?: ISecurityGroup[]; - vpc?: IVpc; + vpc?: Vpc; } & BaseProps; /** diff --git a/lib/rag/index.ts b/lib/rag/index.ts index c5baef5d..a775521a 100644 --- a/lib/rag/index.ts +++ b/lib/rag/index.ts @@ -172,6 +172,7 @@ export class LisaRagStack extends Stack { version: EngineVersion.OPENSEARCH_2_9, enableVersionUpgrade: true, vpc: vpc.vpc, + vpcSubnets: vpc.subnetSelection ? [vpc.subnetSelection] : [], ebs: { enabled: true, volumeSize: ragConfig.opensearchConfig.volumeSize, @@ -249,7 +250,8 @@ export class LisaRagStack extends Stack { description: 'Security group for RAG PGVector database', }); - vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets).forEach((subnet) => { + const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); + subNets?.forEach((subnet) => { pgvectorSg.connections.allowFrom( Peer.ipv4(subnet.ipv4CidrBlock), Port.tcp(ragConfig.rdsConfig?.dbPort || 5432), @@ -262,6 +264,7 @@ export class LisaRagStack extends Stack { const pgvector_db = new DatabaseInstance(this, 'PGVectorDB', { engine: DatabaseInstanceEngine.POSTGRES, vpc: vpc.vpc, + subnetGroup: vpc.subnetGroup, credentials: dbCreds, securityGroups: [pgvectorSg!], removalPolicy: RemovalPolicy.DESTROY, @@ -327,7 +330,7 @@ export class LisaRagStack extends Stack { authorizer, baseEnvironment, config, - vpc: vpc.vpc, + vpc: vpc, commonLayers: [commonLambdaLayer, ragLambdaLayer.layer, sdkLayer], restApiId, rootResourceId, diff --git a/lib/schema.ts b/lib/schema.ts index 2cbdf801..b2eca67b 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -833,6 +833,7 @@ const RawConfigSchema = z }), region: z.string(), vpcId: z.string().optional(), + subnetIds: z.array(z.string().startsWith('subnet-')).optional(), deploymentStage: z.string(), removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]).transform((value) => REMOVAL_POLICIES[value]), runCdkNag: z.boolean().default(false), diff --git a/lib/serve/index.ts b/lib/serve/index.ts index e92dbcd0..621d8978 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -81,7 +81,7 @@ export class LisaServeApplicationStack extends Stack { securityGroup: vpc.securityGroups.restApiAlbSg, taskConfig: config.restApiConfig, tokenTable: tokenTable, - vpc: vpc.vpc, + vpc: vpc, }); const managementKeySecret = new Secret(this, createCdkId([id, 'managementKeySecret']), { @@ -142,7 +142,9 @@ export class LisaServeApplicationStack extends Stack { vpc: vpc.vpc, description: 'Security group for LiteLLM dynamic model management database.', }); - vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets).forEach((subnet) => { + + const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); + subNets?.forEach((subnet) => { litellmDbSg.connections.allowFrom( Peer.ipv4(subnet.ipv4CidrBlock), Port.tcp(rdsConfig.dbPort), @@ -159,6 +161,7 @@ export class LisaServeApplicationStack extends Stack { const litellmDb = new DatabaseInstance(this, 'LiteLLMScalingDB', { engine: DatabaseInstanceEngine.POSTGRES, vpc: vpc.vpc, + subnetGroup: vpc.subnetGroup, credentials: dbCreds, securityGroups: [litellmDbSg!], removalPolicy: config.removalPolicy, diff --git a/lib/stages.ts b/lib/stages.ts index d0059924..247dc932 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -143,7 +143,7 @@ export class LisaServeApplicationStage extends Stage { ...baseStackProps, stackName: createCdkId([config.deploymentName, config.appName, 'API']), description: `LISA-API: ${config.deploymentName}-${config.deploymentStage}`, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); apiBaseStack.addDependency(coreStack); apiBaseStack.addDependency(serveStack); @@ -178,7 +178,7 @@ export class LisaServeApplicationStage extends Stage { description: `LISA-chat: ${config.deploymentName}-${config.deploymentStage}`, restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); chatStack.addDependency(apiBaseStack); chatStack.addDependency(coreStack); diff --git a/test/cdk/stacks/chat.test.ts b/test/cdk/stacks/chat.test.ts index 526e32bf..e97c5c55 100644 --- a/test/cdk/stacks/chat.test.ts +++ b/test/cdk/stacks/chat.test.ts @@ -79,7 +79,7 @@ describe.each(regions)('Chat Nag Pack Tests | Region Test: %s', (awsRegion) => { ...baseStackProps, stackName: createCdkId([config.deploymentName, config.appName, 'API']), description: `LISA-API: ${config.deploymentName}-${config.deploymentStage}`, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); stack = new LisaChatApplicationStack(app, 'LisaChat', { @@ -89,7 +89,7 @@ describe.each(regions)('Chat Nag Pack Tests | Region Test: %s', (awsRegion) => { description: `LISA-chat: ${config.deploymentName}-${config.deploymentStage}`, restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); // WHEN diff --git a/test/cdk/stacks/core-api-base.test.ts b/test/cdk/stacks/core-api-base.test.ts index e14dddde..5dabbee8 100644 --- a/test/cdk/stacks/core-api-base.test.ts +++ b/test/cdk/stacks/core-api-base.test.ts @@ -78,7 +78,7 @@ describe.each(regions)('API Core Nag Pack Tests | Region Test: %s', (awsRegion) ...baseStackProps, stackName: createCdkId([config.deploymentName, config.appName, 'API']), description: `LISA-API: ${config.deploymentName}-${config.deploymentStage}`, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); tempStack.authorizer._attachToApi(tempStack.restApi); diff --git a/test/cdk/stacks/core-api-deploy.test.ts b/test/cdk/stacks/core-api-deploy.test.ts index dc78eb30..7b0fe20e 100644 --- a/test/cdk/stacks/core-api-deploy.test.ts +++ b/test/cdk/stacks/core-api-deploy.test.ts @@ -79,7 +79,7 @@ describe.each(regions)('API Core Deployment Nag Pack Tests | Region Test: %s', ( ...baseStackProps, stackName: createCdkId([config.deploymentName, config.appName, 'API']), description: `LISA-API: ${config.deploymentName}-${config.deploymentStage}`, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); tempStack.authorizer._attachToApi(tempStack.restApi); diff --git a/test/cdk/stacks/ui.test.ts b/test/cdk/stacks/ui.test.ts index 7e95fba9..63066fa4 100644 --- a/test/cdk/stacks/ui.test.ts +++ b/test/cdk/stacks/ui.test.ts @@ -80,7 +80,7 @@ describe.each(regions)('UI Nag Pack Tests | Region Test: %s', (awsRegion) => { ...baseStackProps, stackName: createCdkId([config.deploymentName, config.appName, 'API']), description: `LISA-API: ${config.deploymentName}-${config.deploymentStage}`, - vpc: networkingStack.vpc.vpc, + vpc: networkingStack.vpc, }); stack = new UserInterfaceStack(app, 'LisaUserInterface', { From 634a8407d1e2718a78105388e0147661e86d8c34 Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Mon, 28 Oct 2024 14:42:20 -0400 Subject: [PATCH 11/48] Updates to address some cdk-nag warnings --- lib/api-base/authorizer.ts | 7 ++++ lib/api-base/utils.ts | 7 ++++ lib/models/docker-image-builder.ts | 7 ++++ lib/models/state-machine/create-model.ts | 43 ++++++++++++++++++++++++ lib/models/state-machine/delete-model.ts | 31 +++++++++++++++++ lib/models/state-machine/update-model.ts | 19 +++++++++++ lib/serve/index.ts | 6 ++++ test/cdk/stacks/chat.test.ts | 2 +- test/cdk/stacks/core-api-base.test.ts | 2 +- 9 files changed, 122 insertions(+), 2 deletions(-) diff --git a/lib/api-base/authorizer.ts b/lib/api-base/authorizer.ts index 57661b31..72ac2e21 100644 --- a/lib/api-base/authorizer.ts +++ b/lib/api-base/authorizer.ts @@ -26,6 +26,7 @@ import { BaseProps } from '../schema'; import { createCdkId } from '../core/utils'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; import { Vpc } from '../networking/vpc'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; /** * Properties for RestApiGateway Construct. @@ -75,6 +76,11 @@ export class CustomAuthorizer extends Construct { // Create Lambda authorizer const authorizerLambda = new Function(this, 'AuthorizerLambda', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'AuthorizerLambdaDLQ', { + queueName: 'AuthorizerLambdaDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'authorizer.lambda_functions.lambda_handler', functionName: `${cdk.Stack.of(this).stackName}-lambda-authorizer`, @@ -90,6 +96,7 @@ export class CustomAuthorizer extends Construct { JWT_GROUPS_PROP: config.authConfig!.jwtGroupsProperty, MANAGEMENT_KEY_NAME: managementKeySecretNameStringParameter.stringValue }, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, securityGroups: securityGroups, diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index 1d0b6d6f..05f8949e 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -39,6 +39,7 @@ import { IRole } from 'aws-cdk-lib/aws-iam'; import { Code, Function, Runtime, ILayerVersion, IFunction, CfnPermission } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; import { Vpc } from '../networking/vpc'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; /** * Type representing python lambda function @@ -105,6 +106,11 @@ export function registerAPIEndpoint ( }); } else { handler = new Function(scope, functionId, { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(scope, `${functionId}DLQ`, { + queueName: `${functionId}DLQ`, + enforceSSL: true, + }), functionName: functionId, runtime: pythonRuntime, handler: `${funcDef.resource}.lambda_functions.${funcDef.name}`, @@ -116,6 +122,7 @@ export function registerAPIEndpoint ( timeout: funcDef.timeout || Duration.seconds(180), memorySize: 512, layers, + reservedConcurrentExecutions: 1000, role, vpc: vpc?.vpc, securityGroups, diff --git a/lib/models/docker-image-builder.ts b/lib/models/docker-image-builder.ts index f8190fe6..eb5fa2eb 100644 --- a/lib/models/docker-image-builder.ts +++ b/lib/models/docker-image-builder.ts @@ -24,6 +24,7 @@ import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; import { createCdkId } from '../core/utils'; import { BaseProps } from '../schema'; import { Vpc } from '../networking/vpc'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; export type DockerImageBuilderProps = BaseProps & { ecrUri: string; @@ -116,12 +117,18 @@ export class DockerImageBuilder extends Construct { const functionId = createCdkId([stackName, 'docker-image-builder']); this.dockerImageBuilderFn = new Function(this, functionId, { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'docker-image-builderDLQ', { + queueName: 'docker-image-builderDLQ', + enforceSSL: true, + }), functionName: functionId, runtime: props.config.lambdaConfig.pythonRuntime, handler: 'dockerimagebuilder.handler', code: Code.fromAsset('./lambda/'), timeout: Duration.minutes(1), memorySize: 1024, + reservedConcurrentExecutions: 1000, role: role, environment: { 'LISA_DOCKER_BUCKET': ec2DockerBucket.bucketName, diff --git a/lib/models/state-machine/create-model.ts b/lib/models/state-machine/create-model.ts index 9596a4db..dd199f18 100644 --- a/lib/models/state-machine/create-model.ts +++ b/lib/models/state-machine/create-model.ts @@ -35,6 +35,7 @@ import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; import { Vpc } from '../../networking/vpc'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; type CreateModelStateMachineProps = BaseProps & { modelTable: ITable, @@ -74,11 +75,17 @@ export class CreateModelStateMachine extends Construct { const setModelToCreating = new LambdaInvoke(this, 'SetModelToCreating', { lambdaFunction: new Function(this, 'SetModelToCreatingFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'SetModelToCreatingDLQ', { + queueName: 'SetModelToCreatingDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_set_model_to_creating', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -93,11 +100,17 @@ export class CreateModelStateMachine extends Construct { const startCopyDockerImage = new LambdaInvoke(this, 'StartCopyDockerImage', { lambdaFunction: new Function(this, 'StartCopyDockerImageFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'StartCopyDockerImageDLQ', { + queueName: 'StartCopyDockerImageDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_start_copy_docker_image', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -110,11 +123,17 @@ export class CreateModelStateMachine extends Construct { const pollDockerImageAvailable = new LambdaInvoke(this, 'PollDockerImageAvailable', { lambdaFunction: new Function(this, 'PollDockerImageAvailableFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'PollDockerImageAvailableDLQ', { + queueName: 'PollDockerImageAvailableDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_poll_docker_image_available', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -127,11 +146,17 @@ export class CreateModelStateMachine extends Construct { const handleFailureState = new LambdaInvoke(this, 'HandleFailure', { lambdaFunction: new Function(this, 'HandleFailureFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'HandleFailureDLQ', { + queueName: 'HandleFailureDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_failure', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -150,11 +175,17 @@ export class CreateModelStateMachine extends Construct { const startCreateStack = new LambdaInvoke(this, 'StartCreateStack', { lambdaFunction: new Function(this, 'StartCreateStackFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'StartCreateStackDLQ', { + queueName: 'StartCreateStackDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_start_create_stack', code: Code.fromAsset(config.lambdaSourcePath), timeout: Duration.minutes(8), memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -167,11 +198,17 @@ export class CreateModelStateMachine extends Construct { const pollCreateStack = new LambdaInvoke(this, 'PollCreateStack', { lambdaFunction: new Function(this, 'PollCreateStackFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'PollCreateStackDLQ', { + queueName: 'PollCreateStackDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_poll_create_stack', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -190,11 +227,17 @@ export class CreateModelStateMachine extends Construct { const addModelToLitellm = new LambdaInvoke(this, 'AddModelToLitellm', { lambdaFunction: new Function(this, 'AddModelToLitellmFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'AddModelToLitellmDLQ', { + queueName: 'AddModelToLitellmDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.create_model.handle_add_model_to_litellm', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, diff --git a/lib/models/state-machine/delete-model.ts b/lib/models/state-machine/delete-model.ts index 9d46ef38..54b33690 100644 --- a/lib/models/state-machine/delete-model.ts +++ b/lib/models/state-machine/delete-model.ts @@ -33,6 +33,7 @@ import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { LAMBDA_MEMORY, LAMBDA_TIMEOUT, OUTPUT_PATH, POLLING_TIMEOUT } from './constants'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; import { Vpc } from '../../networking/vpc'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; type DeleteModelStateMachineProps = BaseProps & { modelTable: ITable, @@ -68,11 +69,17 @@ export class DeleteModelStateMachine extends Construct { // Input payload to state machine contains the model name that we want to delete. const setModelToDeleting = new LambdaInvoke(this, 'SetModelToDeleting', { lambdaFunction: new Function(this, 'SetModelToDeletingFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'SetModelToDeletingDLQ', { + queueName: 'SetModelToDeletingDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.delete_model.handle_set_model_to_deleting', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -85,11 +92,17 @@ export class DeleteModelStateMachine extends Construct { const deleteFromLitellm = new LambdaInvoke(this, 'DeleteFromLitellm', { lambdaFunction: new Function(this, 'DeleteFromLitellmFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'DeleteFromLitellmDLQ', { + queueName: 'DeleteFromLitellmDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.delete_model.handle_delete_from_litellm', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -102,11 +115,17 @@ export class DeleteModelStateMachine extends Construct { const deleteStack = new LambdaInvoke(this, 'DeleteStack', { lambdaFunction: new Function(this, 'DeleteStackFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'DeleteStackDLQ', { + queueName: 'DeleteStackDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.delete_model.handle_delete_stack', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -119,11 +138,17 @@ export class DeleteModelStateMachine extends Construct { const monitorDeleteStack = new LambdaInvoke(this, 'MonitorDeleteStack', { lambdaFunction: new Function(this, 'MonitorDeleteStackFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'MonitorDeleteStackDLQ', { + queueName: 'MonitorDeleteStackDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.delete_model.handle_monitor_delete_stack', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -136,11 +161,17 @@ export class DeleteModelStateMachine extends Construct { const deleteFromDdb = new LambdaInvoke(this, 'DeleteFromDdb', { lambdaFunction: new Function(this, 'DeleteFromDdbFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'DeleteFromDdbDLQ', { + queueName: 'DeleteFromDdbDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.delete_model.handle_delete_from_ddb', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, diff --git a/lib/models/state-machine/update-model.ts b/lib/models/state-machine/update-model.ts index 2bbb6fda..2d535c9d 100644 --- a/lib/models/state-machine/update-model.ts +++ b/lib/models/state-machine/update-model.ts @@ -26,6 +26,7 @@ import { LambdaInvoke } from 'aws-cdk-lib/aws-stepfunctions-tasks'; import { LAMBDA_MEMORY, LAMBDA_TIMEOUT, OUTPUT_PATH, POLLING_TIMEOUT } from './constants'; import { Choice, Condition, DefinitionBody, StateMachine, Succeed, Wait, WaitTime } from 'aws-cdk-lib/aws-stepfunctions'; import { Vpc } from '../../networking/vpc'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; type UpdateModelStateMachineProps = BaseProps & { @@ -69,11 +70,17 @@ export class UpdateModelStateMachine extends Construct { const handleJobIntake = new LambdaInvoke(this, 'HandleJobIntake', { lambdaFunction: new Function(this, 'HandleJobIntakeFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'HandleJobIntakeDLQ', { + queueName: 'HandleJobIntakeDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.update_model.handle_job_intake', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -86,11 +93,17 @@ export class UpdateModelStateMachine extends Construct { const handlePollCapacity = new LambdaInvoke(this, 'HandlePollCapacity', { lambdaFunction: new Function(this, 'HandlePollCapacityFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'HandlePollCapacityDLQ', { + queueName: 'HandlePollCapacityDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.update_model.handle_poll_capacity', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -103,11 +116,17 @@ export class UpdateModelStateMachine extends Construct { const handleFinishUpdate = new LambdaInvoke(this, 'HandleFinishUpdate', { lambdaFunction: new Function(this, 'HandleFinishUpdateFunc', { + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'HandleFinishUpdateDLQ', { + queueName: 'HandleFinishUpdateDLQ', + enforceSSL: true, + }), runtime: config.lambdaConfig.pythonRuntime, handler: 'models.state_machine.update_model.handle_finish_update', code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, + reservedConcurrentExecutions: 1000, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, diff --git a/lib/serve/index.ts b/lib/serve/index.ts index 621d8978..8cbba64e 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -101,6 +101,11 @@ export class LisaServeApplicationStack extends Stack { // const rotateManagementKeyLambdaId = createCdkId([id, 'RotateManagementKeyLambda']) // const rotateManagementKeyLambda = new Function(this, rotateManagementKeyLambdaId, { + // deadLetterQueueEnabled: true, + // deadLetterQueue: new Queue(this, 'RotateManagementKeyLambdaDLQ', { + // queueName: 'RotateManagementKeyLambdaDLQ', + // enforceSSL: true, + // }), // functionName: rotateManagementKeyLambdaId, // runtime: config.lambdaConfig.pythonRuntime, // handler: 'management_key.rotate_management_key', @@ -112,6 +117,7 @@ export class LisaServeApplicationStack extends Stack { // }, // layers: [commonLambdaLayer], // vpc: props.vpc.vpc, + // reservedConcurrentExecutions: 1000, // }); // managementKeySecret.grantRead(rotateManagementKeyLambda); diff --git a/test/cdk/stacks/chat.test.ts b/test/cdk/stacks/chat.test.ts index e97c5c55..ac766ff2 100644 --- a/test/cdk/stacks/chat.test.ts +++ b/test/cdk/stacks/chat.test.ts @@ -120,6 +120,6 @@ describe.each(regions)('Chat Nag Pack Tests | Region Test: %s', (awsRegion) => { test('NIST800.53r5 CDK NAG Errors', () => { const errors = Annotations.fromStack(stack).findError('*', Match.stringLikeRegexp('NIST.*')); - expect(errors.length).toBe(14); + expect(errors.length).toBe(4); }); }); diff --git a/test/cdk/stacks/core-api-base.test.ts b/test/cdk/stacks/core-api-base.test.ts index 5dabbee8..c3e543a6 100644 --- a/test/cdk/stacks/core-api-base.test.ts +++ b/test/cdk/stacks/core-api-base.test.ts @@ -112,6 +112,6 @@ describe.each(regions)('API Core Nag Pack Tests | Region Test: %s', (awsRegion) test('NIST800.53r5 CDK NAG Errors', () => { const errors = Annotations.fromStack(stack).findError('*', Match.stringLikeRegexp('NIST.*')); - expect(errors.length).toBe(7); + expect(errors.length).toBe(5); }); }); From cb32c3e0771195b264a7a67b4420ba106f8a0d8e Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Mon, 28 Oct 2024 21:31:47 -0400 Subject: [PATCH 12/48] fixed bad reservedConcurrentExecution values --- lib/api-base/authorizer.ts | 2 +- lib/api-base/utils.ts | 2 +- lib/models/docker-image-builder.ts | 2 +- lib/models/state-machine/create-model.ts | 14 +++++++------- lib/models/state-machine/delete-model.ts | 10 +++++----- lib/models/state-machine/update-model.ts | 6 +++--- lib/serve/index.ts | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/api-base/authorizer.ts b/lib/api-base/authorizer.ts index 72ac2e21..20eed808 100644 --- a/lib/api-base/authorizer.ts +++ b/lib/api-base/authorizer.ts @@ -96,7 +96,7 @@ export class CustomAuthorizer extends Construct { JWT_GROUPS_PROP: config.authConfig!.jwtGroupsProperty, MANAGEMENT_KEY_NAME: managementKeySecretNameStringParameter.stringValue }, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 20, role: role, vpc: vpc?.vpc, securityGroups: securityGroups, diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index 05f8949e..2422742d 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -122,7 +122,7 @@ export function registerAPIEndpoint ( timeout: funcDef.timeout || Duration.seconds(180), memorySize: 512, layers, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 20, role, vpc: vpc?.vpc, securityGroups, diff --git a/lib/models/docker-image-builder.ts b/lib/models/docker-image-builder.ts index eb5fa2eb..5317c7ab 100644 --- a/lib/models/docker-image-builder.ts +++ b/lib/models/docker-image-builder.ts @@ -128,7 +128,7 @@ export class DockerImageBuilder extends Construct { code: Code.fromAsset('./lambda/'), timeout: Duration.minutes(1), memorySize: 1024, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 10, role: role, environment: { 'LISA_DOCKER_BUCKET': ec2DockerBucket.bucketName, diff --git a/lib/models/state-machine/create-model.ts b/lib/models/state-machine/create-model.ts index dd199f18..7d8b2b9b 100644 --- a/lib/models/state-machine/create-model.ts +++ b/lib/models/state-machine/create-model.ts @@ -85,7 +85,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -110,7 +110,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -133,7 +133,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -156,7 +156,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -185,7 +185,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: Duration.minutes(8), memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -208,7 +208,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -237,7 +237,7 @@ export class CreateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, diff --git a/lib/models/state-machine/delete-model.ts b/lib/models/state-machine/delete-model.ts index 54b33690..5a878bca 100644 --- a/lib/models/state-machine/delete-model.ts +++ b/lib/models/state-machine/delete-model.ts @@ -79,7 +79,7 @@ export class DeleteModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -102,7 +102,7 @@ export class DeleteModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -125,7 +125,7 @@ export class DeleteModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -148,7 +148,7 @@ export class DeleteModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -171,7 +171,7 @@ export class DeleteModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, diff --git a/lib/models/state-machine/update-model.ts b/lib/models/state-machine/update-model.ts index 2d535c9d..18fd6c08 100644 --- a/lib/models/state-machine/update-model.ts +++ b/lib/models/state-machine/update-model.ts @@ -80,7 +80,7 @@ export class UpdateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -103,7 +103,7 @@ export class UpdateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, @@ -126,7 +126,7 @@ export class UpdateModelStateMachine extends Construct { code: Code.fromAsset(config.lambdaSourcePath), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, - reservedConcurrentExecutions: 1000, + reservedConcurrentExecutions: 5, role: role, vpc: vpc?.vpc, vpcSubnets: vpc?.subnetSelection, diff --git a/lib/serve/index.ts b/lib/serve/index.ts index 8cbba64e..d8cd2bbe 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -117,7 +117,7 @@ export class LisaServeApplicationStack extends Stack { // }, // layers: [commonLambdaLayer], // vpc: props.vpc.vpc, - // reservedConcurrentExecutions: 1000, + // reservedConcurrentExecutions: 5, // }); // managementKeySecret.grantRead(rotateManagementKeyLambda); From 7b0edd1a2bbf23408bac3eef7fda6660878382a8 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 29 Oct 2024 11:00:23 -0600 Subject: [PATCH 13/48] deployment easement stage 1 --- .github/workflows/test-and-lint.yml | 2 +- .gitignore | 1 + Makefile | 61 +++---- bin/lisa.ts | 21 +-- config-base.yaml | 12 ++ lib/api-base/authorizer.ts | 6 +- lib/api-base/fastApiContainer.ts | 70 +++++--- lib/chat/api/session.ts | 6 +- lib/core/layers/index.ts | 6 +- lib/models/docker-image-builder.ts | 4 +- lib/models/model-api.ts | 18 +- lib/models/state-machine/create-model.ts | 34 ++-- lib/models/state-machine/delete-model.ts | 26 +-- lib/models/state-machine/update-model.ts | 18 +- lib/networking/vpc/index.ts | 2 +- lib/rag/api/repository.ts | 7 +- lib/rag/index.ts | 12 +- lib/schema.ts | 161 +++--------------- lib/serve/index.ts | 10 +- lib/serve/rest-api/src/requirements.txt | 2 +- .../src/utils/generate_litellm_config.py | 12 +- lib/user-interface/index.ts | 5 +- .../react/src/components/utils.ts | 28 --- lib/user-interface/react/src/main.tsx | 8 - package-lock.json | 12 +- package.json | 5 +- scripts/migrate-properties.mjs | 60 +++++++ test/cdk/mocks/config.yaml | 53 +----- 28 files changed, 282 insertions(+), 380 deletions(-) create mode 100644 config-base.yaml create mode 100644 scripts/migrate-properties.mjs diff --git a/.github/workflows/test-and-lint.yml b/.github/workflows/test-and-lint.yml index 6174f1df..f8f320ee 100644 --- a/.github/workflows/test-and-lint.yml +++ b/.github/workflows/test-and-lint.yml @@ -4,7 +4,7 @@ on: push: branches: ['main', 'develop', 'release/**', 'hotfix/**'] pull_request: - branches: ['main', 'develop', 'release/**', 'hotfix/**'] + branches: ['main', 'develop', 'release/**', 'hotfix/**', 'feature/**'] permissions: contents: read diff --git a/.gitignore b/.gitignore index 5ffdff8e..7ad88b97 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ coverage /models # Deployment configuration file config.yaml +config-custom.yaml diff --git a/Makefile b/Makefile index d66f1e80..c478e825 100644 --- a/Makefile +++ b/Makefile @@ -14,50 +14,45 @@ HEADLESS = false # Arguments defined through command line or config.yaml -# ENV -ifeq (${ENV},) -ENV := $(shell cat $(PROJECT_DIR)/config.yaml | yq '.env') -endif - -ifeq (${ENV},) -$(error env must be set in command line using ENV variable or config.yaml) -endif - # PROFILE (optional argument) ifeq (${PROFILE},) -TEMP_PROFILE := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).profile) +TEMP_PROFILE := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .profile) ifneq ($(TEMP_PROFILE), null) PROFILE := ${TEMP_PROFILE} else -$(warning profile is not set in the command line using PROFILE variable or config.yaml, attempting deployment without this variable) +$(warning profile is not set in the command line using PROFILE variable or config files, attempting deployment without this variable) endif endif # DEPLOYMENT_NAME ifeq (${DEPLOYMENT_NAME},) -DEPLOYMENT_NAME := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).deploymentName) +DEPLOYMENT_NAME := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .deploymentName) endif -ifeq (${DEPLOYMENT_NAME},) -$(error deploymentName must be set in command line using DEPLOYMENT_NAME variable or config.yaml) +ifeq (${DEPLOYMENT_NAME}, null) +DEPLOYMENT_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .deploymentName) +endif + +ifeq (${DEPLOYMENT_NAME}, null) +$(error deploymentName must be set in command line using DEPLOYMENT_NAME variable or config files) endif # ACCOUNT_NUMBER ifeq (${ACCOUNT_NUMBER},) -ACCOUNT_NUMBER := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).accountNumber) +ACCOUNT_NUMBER := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .accountNumber) endif ifeq (${ACCOUNT_NUMBER},) -$(error accountNumber must be set in command line using ACCOUNT_NUMBER variable or config.yaml) +$(error accountNumber must be set in command line using ACCOUNT_NUMBER variable or config files) endif # REGION ifeq (${REGION},) -REGION := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).region) +REGION := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .region) endif ifeq (${REGION},) -$(error region must be set in command line using REGION variable or config.yaml) +$(error region must be set in command line using REGION variable or config files) endif # URL_SUFFIX - used for the docker login @@ -67,22 +62,30 @@ else URL_SUFFIX := c2s.ic.gov endif -# Arguments defined through config.yaml +# Arguments defined through config files # APP_NAME -APP_NAME := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).appName) -ifeq (${APP_NAME},) -$(error appName must be set in config.yaml) +APP_NAME := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .appName) +ifeq (${APP_NAME}, null) +APP_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .appName) +endif + +ifeq (${APP_NAME}, null) +APP_NAME := lisa endif # DEPLOYMENT_STAGE -DEPLOYMENT_STAGE := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).deploymentStage) -ifeq (${DEPLOYMENT_STAGE},) -$(error deploymentStage must be set in config.yaml) +DEPLOYMENT_STAGE := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .deploymentStage) +ifeq (${DEPLOYMENT_STAGE}, null) +DEPLOYMENT_STAGE := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .deploymentStage) +endif + +ifeq (${DEPLOYMENT_STAGE}, null) +$(error deploymentStage must be set in config files) endif # ACCOUNT_NUMBERS_ECR - AWS account numbers that need to be logged into with Docker CLI to use ECR -ACCOUNT_NUMBERS_ECR := $(shell cat $(PROJECT_DIR)/config.yaml | yq .$(ENV).accountNumbersEcr[]) +ACCOUNT_NUMBERS_ECR := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq .accountNumbersEcr[]) # Append deployed account number to array for dockerLogin rule ACCOUNT_NUMBERS_ECR := $(ACCOUNT_NUMBERS_ECR) $(ACCOUNT_NUMBER) @@ -97,10 +100,10 @@ ifneq ($(findstring $(DEPLOYMENT_STAGE),$(STACK)),$(DEPLOYMENT_STAGE)) endif # MODEL_IDS - IDs of models to deploy -MODEL_IDS := $(shell cat $(PROJECT_DIR)/config.yaml | yq '.$(ENV).ecsModels[].modelName') +MODEL_IDS := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.ecsModels[].modelName') # MODEL_BUCKET - S3 bucket containing model artifacts -MODEL_BUCKET := $(shell cat $(PROJECT_DIR)/config.yaml | yq '.$(ENV).s3BucketModels') +MODEL_BUCKET := $(shell cat $(PROJECT_DIR)/config-custom.yaml | yq '.s3BucketModels') ################################################################################# @@ -181,7 +184,7 @@ modelCheck: echo "What is your huggingface access token? "; \ read -s access_token; \ echo "Converting and uploading safetensors for model: $(MODEL_ID)"; \ - tgiImage=$$(yq -r '[.$(ENV).ecsModels[] | select(.inferenceContainer == "tgi") | .baseImage] | first' $(PROJECT_DIR)/config.yaml); \ + tgiImage=$$(yq -r '[.ecsModels[] | select(.inferenceContainer == "tgi") | .baseImage] | first' $(PROJECT_DIR)/config-custom.yaml); \ echo $$tgiImage; \ $(PROJECT_DIR)/scripts/convert-and-upload-model.sh -m $(MODEL_ID) -s $(MODEL_BUCKET) -a $$access_token -t $$tgiImage -d $$localModelDir; \ fi; \ diff --git a/bin/lisa.ts b/bin/lisa.ts index b7763272..fbc6b305 100644 --- a/bin/lisa.ts +++ b/bin/lisa.ts @@ -22,23 +22,17 @@ import * as path from 'path'; import * as cdk from 'aws-cdk-lib'; import * as yaml from 'js-yaml'; +import _ from 'lodash'; import { Config, ConfigFile, ConfigSchema } from '../lib/schema'; import { LisaServeApplicationStage } from '../lib/stages'; -// Read configuration file -const configFilePath = path.join(__dirname, '../config.yaml'); -const configFile = yaml.load(fs.readFileSync(configFilePath, 'utf8')) as ConfigFile; -let configEnv = configFile.env || 'dev'; - -// Select configuration environment -if (process.env.ENV) { - configEnv = process.env.ENV; -} -const configData = configFile[configEnv]; -if (!configData) { - throw new Error(`Configuration for environment "${configEnv}" not found.`); -} +// Read configuration files +const baseConfigFilePath = path.join(__dirname, '../config-base.yaml'); +const customConfigFilePath = path.join(__dirname, '../config-custom.yaml'); +const baseConfigFile = yaml.load(fs.readFileSync(baseConfigFilePath, 'utf8')) as ConfigFile; +const customConfigFile = yaml.load(fs.readFileSync(customConfigFilePath, 'utf8')) as ConfigFile; +const configData = _.merge(baseConfigFile, customConfigFile); // Other command line argument overrides type EnvMapping = [string, keyof Config]; @@ -59,6 +53,7 @@ mappings.forEach(([envVar, configVar]) => { let config: Config; try { config = ConfigSchema.parse(configData); + console.log('MERGED CONFIG FILE:\n' + yaml.dump(config)); } catch (error) { if (error instanceof Error) { console.error('Error parsing the configuration:', error.message); diff --git a/config-base.yaml b/config-base.yaml new file mode 100644 index 00000000..95f25aa0 --- /dev/null +++ b/config-base.yaml @@ -0,0 +1,12 @@ +mountS3DebUrl: https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.deb +stackSynthesizer: CliCredentialsStackSynthesizer +ragRepositories: + - repositoryId: pgvector-rag + type: pgvector + rdsConfig: + username: postgres +ragFileProcessingConfig: + chunkSize: 512 + chunkOverlap: 51 +litellmConfig: + db_key: sk-a8814208-0388-480c-9fc7-fea59607ca38 diff --git a/lib/api-base/authorizer.ts b/lib/api-base/authorizer.ts index 20eed808..b80c0035 100644 --- a/lib/api-base/authorizer.ts +++ b/lib/api-base/authorizer.ts @@ -18,7 +18,7 @@ import * as cdk from 'aws-cdk-lib'; import { RequestAuthorizer, IdentitySource } from 'aws-cdk-lib/aws-apigateway'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; -import { Code, Function, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Code, Function, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; @@ -81,10 +81,10 @@ export class CustomAuthorizer extends Construct { queueName: 'AuthorizerLambdaDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'authorizer.lambda_functions.lambda_handler', functionName: `${cdk.Stack.of(this).stackName}-lambda-authorizer`, - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), description: 'REST API and UI Authorization Lambda', timeout: cdk.Duration.seconds(30), memorySize: 128, diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index 1c79492b..2f4ca3a6 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -23,7 +23,7 @@ import { Construct } from 'constructs'; import { dump as yamlDump } from 'js-yaml'; import { ECSCluster } from './ecsCluster'; -import { BaseProps, Ec2Metadata, EcsSourceType, FastApiContainerConfig } from '../schema'; +import { BaseProps, Ec2Metadata, EcsSourceType } from '../schema'; import { Vpc } from '../networking/vpc'; // This is the amount of memory to buffer (or subtract off) from the total instance memory, if we don't include this, @@ -35,13 +35,11 @@ const CONTAINER_MEMORY_BUFFER = 1024 * 2; * * @property {IVpc} vpc - The virtual private cloud (VPC). * @property {SecurityGroup} securityGroups - The security groups of the application. - * @property {Map} importedSubnets for application. */ type FastApiContainerProps = { apiName: string; resourcePath: string; securityGroup: SecurityGroup; - taskConfig: FastApiContainerConfig; tokenTable: ITable | undefined; vpc: Vpc; } & BaseProps; @@ -67,23 +65,20 @@ export class FastApiContainer extends Construct { constructor (scope: Construct, id: string, props: FastApiContainerProps) { super(scope, id); - const { config, securityGroup, taskConfig, tokenTable, vpc } = props; + const { config, securityGroup, tokenTable, vpc } = props; - let buildArgs: Record | undefined = undefined; - if (taskConfig.containerConfig.image.type === EcsSourceType.ASSET) { - buildArgs = { - BASE_IMAGE: taskConfig.containerConfig.image.baseImage, - PYPI_INDEX_URL: config.pypiConfig.indexUrl, - PYPI_TRUSTED_HOST: config.pypiConfig.trustedHost, - LITELLM_CONFIG: yamlDump(config.litellmConfig), - }; - } + const buildArgs: Record | undefined = { + BASE_IMAGE: 'python:3.10', + PYPI_INDEX_URL: config.pypiConfig.indexUrl, + PYPI_TRUSTED_HOST: config.pypiConfig.trustedHost, + LITELLM_CONFIG: yamlDump(config.litellmConfig), + }; const environment: Record = { LOG_LEVEL: config.logLevel, AWS_REGION: config.region, AWS_REGION_NAME: config.region, // for supporting SageMaker endpoints in LiteLLM - THREADS: Ec2Metadata.get(taskConfig.instanceType).vCpus.toString(), - LITELLM_KEY: config.litellmConfig.general_settings.master_key, + THREADS: Ec2Metadata.get('m5.large').vCpus.toString(), + LITELLM_KEY: config.litellmConfig.db_key, }; if (config.restApiConfig.internetFacing) { @@ -104,15 +99,52 @@ export class FastApiContainer extends Construct { config, ecsConfig: { amiHardwareType: AmiHardwareType.STANDARD, - autoScalingConfig: taskConfig.autoScalingConfig, + autoScalingConfig: { + blockDeviceVolumeSize: 30, + minCapacity: 1, + maxCapacity: 1, + cooldown: 60, + defaultInstanceWarmup: 60, + metricConfig: { + AlbMetricName: 'RequestCountPerTarget', + targetValue: 1000, + duration: 60, + estimatedInstanceWarmup: 30 + } + }, buildArgs, - containerConfig: taskConfig.containerConfig, + containerConfig: { + image: { + baseImage: 'python:3.10', + path: 'lib/serve/rest-api', + type: EcsSourceType.ASSET + }, + healthCheckConfig: { + command: ['CMD-SHELL', 'exit 0'], + interval: 10, + startPeriod: 30, + timeout: 5, + retries: 3 + }, + environment: {}, + sharedMemorySize: 0 + }, containerMemoryBuffer: CONTAINER_MEMORY_BUFFER, environment, identifier: props.apiName, - instanceType: taskConfig.instanceType, + instanceType: 'm5.large', internetFacing: config.restApiConfig.internetFacing, - loadBalancerConfig: taskConfig.loadBalancerConfig, + loadBalancerConfig: { + healthCheckConfig: { + path: '/health', + interval: 60, + timeout: 30, + healthyThresholdCount: 2, + unhealthyThresholdCount: 10 + }, + domainName: config.restApiConfig.domainName, + sslCertIamArn: config.restApiConfig?.sslCertIamArn ?? null, + }, }, securityGroup, vpc diff --git a/lib/chat/api/session.ts b/lib/chat/api/session.ts index d80eccc8..cd81b309 100644 --- a/lib/chat/api/session.ts +++ b/lib/chat/api/session.ts @@ -18,7 +18,7 @@ import { IAuthorizer, RestApi } from 'aws-cdk-lib/aws-apigateway'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { Role } from 'aws-cdk-lib/aws-iam'; -import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; @@ -153,10 +153,10 @@ export class SessionApi extends Construct { this, restApi, authorizer, - config.lambdaSourcePath, + './lambda', [commonLambdaLayer], f, - config.lambdaConfig.pythonRuntime, + Runtime.PYTHON_3_10, lambdaRole, vpc, securityGroups, diff --git a/lib/core/layers/index.ts b/lib/core/layers/index.ts index 82b3c171..adf1200f 100644 --- a/lib/core/layers/index.ts +++ b/lib/core/layers/index.ts @@ -15,7 +15,7 @@ */ import { BundlingOutput } from 'aws-cdk-lib'; -import { Architecture, Code, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Architecture, Code, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; import { Construct } from 'constructs'; @@ -84,7 +84,7 @@ export class Layer extends Construct { const layerAsset = new Asset(this, 'LayerAsset', { path, bundling: { - image: config.lambdaConfig.pythonRuntime.bundlingImage, + image: Runtime.PYTHON_3_10.bundlingImage, platform: architecture.dockerPlatform, command: ['bash', '-c', `set -e ${args.join(' ')}`], outputType: BundlingOutput.AUTO_DISCOVER, @@ -97,7 +97,7 @@ export class Layer extends Construct { const layer = new LayerVersion(this, 'Layer', { code: layerCode, - compatibleRuntimes: [config.lambdaConfig.pythonRuntime], + compatibleRuntimes: [Runtime.PYTHON_3_10], removalPolicy: config.removalPolicy, description: description, }); diff --git a/lib/models/docker-image-builder.ts b/lib/models/docker-image-builder.ts index 5317c7ab..da2174d9 100644 --- a/lib/models/docker-image-builder.ts +++ b/lib/models/docker-image-builder.ts @@ -15,7 +15,7 @@ */ import { Construct } from 'constructs'; -import { Code, Function } from 'aws-cdk-lib/aws-lambda'; +import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Role, InstanceProfile, ServicePrincipal, ManagedPolicy, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Stack, Duration } from 'aws-cdk-lib'; import { Bucket } from 'aws-cdk-lib/aws-s3'; @@ -123,7 +123,7 @@ export class DockerImageBuilder extends Construct { enforceSSL: true, }), functionName: functionId, - runtime: props.config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'dockerimagebuilder.handler', code: Code.fromAsset('./lambda/'), timeout: Duration.minutes(1), diff --git a/lib/models/model-api.ts b/lib/models/model-api.ts index 333228ce..82875943 100644 --- a/lib/models/model-api.ts +++ b/lib/models/model-api.ts @@ -28,7 +28,7 @@ import { Role, ServicePrincipal, } from 'aws-cdk-lib/aws-iam'; -import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; @@ -253,8 +253,8 @@ export class ModelsApi extends Construct { const environment = { LISA_API_URL_PS_NAME: lisaServeEndpointUrlPs.parameterName, - REST_API_VERSION: config.restApiConfig.apiVersion, - RESTAPI_SSL_CERT_ARN: config.restApiConfig.loadBalancerConfig.sslCertIamArn ?? '', + REST_API_VERSION: 'v2', + RESTAPI_SSL_CERT_ARN: config.restApiConfig?.sslCertIamArn ?? '', CREATE_SFN_ARN: createModelStateMachine.stateMachineArn, DELETE_SFN_ARN: deleteModelStateMachine.stateMachineArn, UPDATE_SFN_ARN: updateModelStateMachine.stateMachineArn, @@ -267,7 +267,7 @@ export class ModelsApi extends Construct { this, restApi, authorizer, - config.lambdaSourcePath, + './lambda', [commonLambdaLayer, fastapiLambdaLayer], { name: 'handler', @@ -277,19 +277,19 @@ export class ModelsApi extends Construct { method: 'ANY', environment }, - config.lambdaConfig.pythonRuntime, + Runtime.PYTHON_3_10, lambdaRole, vpc, securityGroups, ); lisaServeEndpointUrlPs.grantRead(lambdaFunction.role!); - if (config.restApiConfig.loadBalancerConfig.sslCertIamArn) { + if (config.restApiConfig?.sslCertIamArn) { const certPerms = new Policy(this, 'ModelsApiCertPerms', { statements: [ new PolicyStatement({ actions: ['iam:GetServerCertificate'], - resources: [config.restApiConfig.loadBalancerConfig.sslCertIamArn], + resources: [config.restApiConfig?.sslCertIamArn], effect: Effect.ALLOW, }) ] @@ -346,10 +346,10 @@ export class ModelsApi extends Construct { this, restApi, authorizer, - config.lambdaSourcePath, + './lambda', [commonLambdaLayer], f, - config.lambdaConfig.pythonRuntime, + Runtime.PYTHON_3_10, lambdaRole, vpc, securityGroups, diff --git a/lib/models/state-machine/create-model.ts b/lib/models/state-machine/create-model.ts index 7d8b2b9b..d76ec044 100644 --- a/lib/models/state-machine/create-model.ts +++ b/lib/models/state-machine/create-model.ts @@ -27,7 +27,7 @@ import { Construct } from 'constructs'; import { Duration } from 'aws-cdk-lib'; import { BaseProps } from '../../schema'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; -import { Code, Function, ILayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Code, Function, ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { LAMBDA_MEMORY, LAMBDA_TIMEOUT, OUTPUT_PATH, POLLING_TIMEOUT } from './constants'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; @@ -68,9 +68,9 @@ export class CreateModelStateMachine extends Construct { ECS_MODEL_DEPLOYER_FN_ARN: ecsModelDeployerFnArn, LISA_API_URL_PS_NAME: restApiContainerEndpointPs.parameterName, MODEL_TABLE_NAME: modelTable.tableName, - REST_API_VERSION: config.restApiConfig.apiVersion, + REST_API_VERSION: 'v2', MANAGEMENT_KEY_NAME: managementKeyName, - RESTAPI_SSL_CERT_ARN: config.restApiConfig.loadBalancerConfig.sslCertIamArn ?? '', + RESTAPI_SSL_CERT_ARN: config.restApiConfig?.sslCertIamArn ?? '', }; const setModelToCreating = new LambdaInvoke(this, 'SetModelToCreating', { @@ -80,9 +80,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'SetModelToCreatingDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_set_model_to_creating', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -105,9 +105,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'StartCopyDockerImageDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_start_copy_docker_image', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -128,9 +128,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'PollDockerImageAvailableDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_poll_docker_image_available', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -151,9 +151,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'HandleFailureDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_failure', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -180,9 +180,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'StartCreateStackDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_start_create_stack', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: Duration.minutes(8), memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -203,9 +203,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'PollCreateStackDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_poll_create_stack', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -232,9 +232,9 @@ export class CreateModelStateMachine extends Construct { queueName: 'AddModelToLitellmDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.create_model.handle_add_model_to_litellm', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, diff --git a/lib/models/state-machine/delete-model.ts b/lib/models/state-machine/delete-model.ts index 5a878bca..29b2389e 100644 --- a/lib/models/state-machine/delete-model.ts +++ b/lib/models/state-machine/delete-model.ts @@ -25,7 +25,7 @@ import { Succeed, Wait, } from 'aws-cdk-lib/aws-stepfunctions'; -import { Code, Function, ILayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Code, Function, ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { BaseProps } from '../../schema'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; @@ -60,9 +60,9 @@ export class DeleteModelStateMachine extends Construct { const environment = { // Environment variables to set in all Lambda functions MODEL_TABLE_NAME: modelTable.tableName, LISA_API_URL_PS_NAME: restApiContainerEndpointPs.parameterName, - REST_API_VERSION: config.restApiConfig.apiVersion, + REST_API_VERSION: 'v2', MANAGEMENT_KEY_NAME: managementKeyName, - RESTAPI_SSL_CERT_ARN: config.restApiConfig.loadBalancerConfig.sslCertIamArn ?? '', + RESTAPI_SSL_CERT_ARN: config.restApiConfig?.sslCertIamArn ?? '', }; // Needs to return if model has a stack to delete or if it is only in LiteLLM. Updates model state to DELETING. @@ -74,9 +74,9 @@ export class DeleteModelStateMachine extends Construct { queueName: 'SetModelToDeletingDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.delete_model.handle_set_model_to_deleting', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -97,9 +97,9 @@ export class DeleteModelStateMachine extends Construct { queueName: 'DeleteFromLitellmDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.delete_model.handle_delete_from_litellm', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -120,9 +120,9 @@ export class DeleteModelStateMachine extends Construct { queueName: 'DeleteStackDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.delete_model.handle_delete_stack', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -143,9 +143,9 @@ export class DeleteModelStateMachine extends Construct { queueName: 'MonitorDeleteStackDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.delete_model.handle_monitor_delete_stack', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -166,9 +166,9 @@ export class DeleteModelStateMachine extends Construct { queueName: 'DeleteFromDdbDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.delete_model.handle_delete_from_ddb', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, diff --git a/lib/models/state-machine/update-model.ts b/lib/models/state-machine/update-model.ts index 18fd6c08..aa59e989 100644 --- a/lib/models/state-machine/update-model.ts +++ b/lib/models/state-machine/update-model.ts @@ -17,7 +17,7 @@ import { BaseProps } from '../../schema'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; -import { Code, Function, ILayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Code, Function, ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IStringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -63,9 +63,9 @@ export class UpdateModelStateMachine extends Construct { const environment = { // Environment variables to set in all Lambda functions MODEL_TABLE_NAME: modelTable.tableName, LISA_API_URL_PS_NAME: restApiContainerEndpointPs.parameterName, - REST_API_VERSION: config.restApiConfig.apiVersion, + REST_API_VERSION: 'v2', MANAGEMENT_KEY_NAME: managementKeyName, - RESTAPI_SSL_CERT_ARN: config.restApiConfig.loadBalancerConfig.sslCertIamArn ?? '', + RESTAPI_SSL_CERT_ARN: config.restApiConfig?.sslCertIamArn ?? '', }; const handleJobIntake = new LambdaInvoke(this, 'HandleJobIntake', { @@ -75,9 +75,9 @@ export class UpdateModelStateMachine extends Construct { queueName: 'HandleJobIntakeDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.update_model.handle_job_intake', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -98,9 +98,9 @@ export class UpdateModelStateMachine extends Construct { queueName: 'HandlePollCapacityDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.update_model.handle_poll_capacity', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, @@ -121,9 +121,9 @@ export class UpdateModelStateMachine extends Construct { queueName: 'HandleFinishUpdateDLQ', enforceSSL: true, }), - runtime: config.lambdaConfig.pythonRuntime, + runtime: Runtime.PYTHON_3_10, handler: 'models.state_machine.update_model.handle_finish_update', - code: Code.fromAsset(config.lambdaSourcePath), + code: Code.fromAsset('./lambda'), timeout: LAMBDA_TIMEOUT, memorySize: LAMBDA_MEMORY, reservedConcurrentExecutions: 5, diff --git a/lib/networking/vpc/index.ts b/lib/networking/vpc/index.ts index 414123d0..1f8436f1 100644 --- a/lib/networking/vpc/index.ts +++ b/lib/networking/vpc/index.ts @@ -145,7 +145,7 @@ export class Vpc extends Construct { // All HTTP VPC traffic -> ECS model ALB ecsModelAlbSg.addIngressRule(Peer.ipv4(vpc.vpcCidrBlock), Port.tcp(80), 'Allow VPC traffic on port 80'); - if (config.restApiConfig.loadBalancerConfig.sslCertIamArn) { + if (config.restApiConfig?.sslCertIamArn) { // All HTTPS IPV4 traffic -> REST API ALB restApiAlbSg.addIngressRule(Peer.anyIpv4(), Port.tcp(443), 'Allow any traffic on port 443'); } else { diff --git a/lib/rag/api/repository.ts b/lib/rag/api/repository.ts index d0633058..06cae2f9 100644 --- a/lib/rag/api/repository.ts +++ b/lib/rag/api/repository.ts @@ -18,7 +18,7 @@ import { Duration } from 'aws-cdk-lib'; import { IAuthorizer, RestApi } from 'aws-cdk-lib/aws-apigateway'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; -import { ILayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { ILayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; import { PythonLambdaFunction, registerAPIEndpoint } from '../../api-base/utils'; @@ -58,7 +58,6 @@ export class RepositoryApi extends Construct { const { authorizer, baseEnvironment, - config, commonLayers, lambdaExecutionRole, restApiId, @@ -131,10 +130,10 @@ export class RepositoryApi extends Construct { this, restApi, authorizer, - config.lambdaSourcePath, + './lambda', commonLayers, f, - config.lambdaConfig.pythonRuntime, + Runtime.PYTHON_3_10, lambdaExecutionRole, vpc, securityGroups, diff --git a/lib/rag/index.ts b/lib/rag/index.ts index a775521a..30460f86 100644 --- a/lib/rag/index.ts +++ b/lib/rag/index.ts @@ -24,7 +24,7 @@ import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; import { IAuthorizer } from 'aws-cdk-lib/aws-apigateway'; import { ISecurityGroup, Peer, Port, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { AnyPrincipal, CfnServiceLinkedRole, Effect, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; -import { Code, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Code, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { Domain, EngineVersion, IDomain } from 'aws-cdk-lib/aws-opensearchservice'; import { Credentials, DatabaseInstance, DatabaseInstanceEngine } from 'aws-cdk-lib/aws-rds'; import { Bucket, HttpMethods } from 'aws-cdk-lib/aws-s3'; @@ -98,12 +98,12 @@ export class LisaRagStack extends Stack { CHUNK_SIZE: config.ragFileProcessingConfig!.chunkSize.toString(), CHUNK_OVERLAP: config.ragFileProcessingConfig!.chunkOverlap.toString(), LISA_API_URL_PS_NAME: endpointUrl.parameterName, - REST_API_VERSION: config.restApiConfig.apiVersion, + REST_API_VERSION: 'v2', }; // Add REST API SSL Cert ARN if it exists to be used to verify SSL calls to REST API - if (config.restApiConfig.loadBalancerConfig.sslCertIamArn) { - baseEnvironment['RESTAPI_SSL_CERT_ARN'] = config.restApiConfig.loadBalancerConfig.sslCertIamArn; + if (config.restApiConfig?.sslCertIamArn) { + baseEnvironment['RESTAPI_SSL_CERT_ARN'] = config.restApiConfig?.sslCertIamArn; } const lambdaRole = Role.fromRoleArn( @@ -312,14 +312,14 @@ export class LisaRagStack extends Stack { if (config.lambdaLayerAssets?.sdkLayerPath) { sdkLayer = new LayerVersion(this, 'SdkLayer', { code: Code.fromAsset(config.lambdaLayerAssets?.sdkLayerPath), - compatibleRuntimes: [config.lambdaConfig.pythonRuntime], + compatibleRuntimes: [Runtime.PYTHON_3_10], removalPolicy: config.removalPolicy, description: 'LISA SDK common layer', }); } else { sdkLayer = new PythonLayerVersion(this, 'SdkLayer', { entry: SDK_PATH, - compatibleRuntimes: [config.lambdaConfig.pythonRuntime], + compatibleRuntimes: [Runtime.PYTHON_3_10], removalPolicy: config.removalPolicy, description: 'LISA SDK common layer', }); diff --git a/lib/schema.ts b/lib/schema.ts index b2eca67b..7e780ffe 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -21,35 +21,17 @@ import * as path from 'path'; import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import { AmiHardwareType } from 'aws-cdk-lib/aws-ecs'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; import { z } from 'zod'; const HERE: string = path.resolve(__dirname); const VERSION_PATH: string = path.resolve(HERE, '..', 'VERSION'); const VERSION: string = fs.readFileSync(VERSION_PATH, 'utf8').trim(); -const PYTHON_VERSIONS: Record = { - PYTHON_3_8: lambda.Runtime.PYTHON_3_8, - PYTHON_3_9: lambda.Runtime.PYTHON_3_9, - PYTHON_3_10: lambda.Runtime.PYTHON_3_10, - PYTHON_3_11: lambda.Runtime.PYTHON_3_11, -}; const REMOVAL_POLICIES: Record = { destroy: cdk.RemovalPolicy.DESTROY, retain: cdk.RemovalPolicy.RETAIN, }; -/** - * Configuration schema for Lambda. - */ -const lambdaConfigSchema = z.object({ - pythonRuntime: z - .union([z.literal('PYTHON_3_8'), z.literal('PYTHON_3_9'), z.literal('PYTHON_3_10'), z.literal('PYTHON_3_11')]) - .default('PYTHON_3_9') - .transform((value) => PYTHON_VERSIONS[value]), - logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]), -}); - /** * Enum for different types of models. */ @@ -563,24 +545,18 @@ const RdsInstanceConfig = z.object({ /** * Configuration schema for REST API. * - * @property {string} instanceType - EC2 instance type. - * @property {ContainerConfig} containerConfig - Configuration for the container. - * @property {AutoScalingConfigSchema} autoScalingConfig - Configuration for auto scaling settings. - * @property {LoadBalancerConfig} loadBalancerConfig - Configuration for load balancer settings. - * @property {boolean} [internetFacing=true] - Whether or not the REST API ALB will be configured as internet facing. - * @property {RdsInstanceConfig} rdsConfig - Configuration for LiteLLM scaling database. + * @property {boolean} [internetFacing=true] - Whether the REST API ALB will be configured as internet facing. + * @property {string} sslCertIamArn - ARN of the self-signed cert to be used throughout the system */ const FastApiContainerConfigSchema = z.object({ - apiVersion: z.literal('v2'), - instanceType: z.enum(VALID_INSTANCE_KEYS), - containerConfig: ContainerConfigSchema, - autoScalingConfig: AutoScalingConfigSchema, - loadBalancerConfig: LoadBalancerConfigSchema, internetFacing: z.boolean().default(true), + domainName: z.string().optional().nullable().default(null), + sslCertIamArn: z.string().optional().nullable().default(null), rdsConfig: RdsInstanceConfig.optional() .default({ dbName: 'postgres', username: 'postgres', + dbPort: 5432 }) .refine( (config) => { @@ -588,8 +564,8 @@ const FastApiContainerConfigSchema = z.object({ }, { message: - 'We do not allow using an existing DB for LiteLLM because of its requirement in internal model management ' + - 'APIs. Please do not define the dbHost or passwordSecretId fields for the FastAPI container DB config.', + 'We do not allow using an existing DB for LiteLLM because of its requirement in internal model management ' + + 'APIs. Please do not define the dbHost or passwordSecretId fields for the FastAPI container DB config.', }, ), }); @@ -674,111 +650,16 @@ const ApiGatewayConfigSchema = z }) .optional(); -/** - * Configuration for models inside the LiteLLM Config - * See https://litellm.vercel.app/docs/proxy/configs#all-settings for more details. - * - * The `lisa_params` are custom for the LISA installation to add model metadata to allow the models to be referenced - * correctly within the Chat UI. LiteLLM will ignore these parameters as it is not looking for them, and it will not - * fail to initialize as a result of them existing. - */ -const LiteLLMModel = z.object({ - model_name: z.string(), - litellm_params: z.object({ - model: z.string(), - api_base: z.string().optional(), - api_key: z.string().optional(), - aws_region_name: z.string().optional(), - }), - lisa_params: z - .object({ - streaming: z.boolean().nullable().default(null), - model_type: z.nativeEnum(ModelType), - }) - .refine( - (data) => { - // 'textgen' type must have boolean streaming, 'embedding' type must have null streaming - const isValidForTextgen = data.model_type === 'textgen' && typeof data.streaming === 'boolean'; - const isValidForEmbedding = data.model_type === 'embedding' && data.streaming === null; - - return isValidForTextgen || isValidForEmbedding; - }, - { - message: `For 'textgen' models, 'streaming' must be true or false. - For 'embedding' models, 'streaming' must not be set.`, - path: ['streaming'], - }, - ), - model_info: z - .object({ - id: z.string().optional(), - mode: z.string().optional(), - input_cost_per_token: z.number().optional(), - output_cost_per_token: z.number().optional(), - max_tokens: z.number().optional(), - base_model: z.string().optional(), - }) - .optional(), -}); - /** * Core LiteLLM configuration. * See https://litellm.vercel.app/docs/proxy/configs#all-settings for more details about each field. */ const LiteLLMConfig = z.object({ - environment_variables: z.map(z.string(), z.string()).optional(), - model_list: z - .array(LiteLLMModel) - .optional() - .nullable() - .default([]) - .transform((value) => value ?? []), - litellm_settings: z.object({ - // ALL (https://github.com/BerriAI/litellm/blob/main/litellm/__init__.py) - telemetry: z.boolean().default(false).optional(), - drop_params: z.boolean().default(true).optional(), - }), - general_settings: z - .object({ - completion_model: z.string().optional(), - disable_spend_logs: z.boolean().optional(), // turn off writing each transaction to the db - disable_master_key_return: z.boolean().optional(), // turn off returning master key on UI - disable_reset_budget: z.boolean().optional(), // turn off reset budget scheduled task - enable_jwt_auth: z.boolean().optional(), // allow proxy admin to auth in via jwt tokens with 'litellm_proxy_admin' - enforce_user_param: z.boolean().optional(), // requires all openai endpoint requests to have a 'user' param - allowed_routes: z.array(z.string()).optional(), // list of allowed proxy API routes a user can access. (JWT only) - key_management_system: z.string().optional(), // either google_kms or azure_kms - master_key: z.string().refine( - (key) => key.startsWith('sk-'), // key needed for model management actions - 'Key string must be defined for model management operations, and it must start with "sk-".' + - 'This can be any string, and a random UUID is recommended. Example: sk-f132c7cc-059c-481b-b5ca-a42e191672aa', - ), - database_url: z.string().optional(), - database_connection_pool_limit: z.number().optional(), // default 100 - database_connection_timeout: z.number().optional(), // default 60s - database_type: z.string().optional(), - database_args: z - .object({ - billing_mode: z.string().optional(), - read_capacity_units: z.number().optional(), - write_capacity_units: z.number().optional(), - ssl_verify: z.boolean().optional(), - region_name: z.string().optional(), - user_table_name: z.string().optional(), - key_table_name: z.string().optional(), - config_table_name: z.string().optional(), - spend_table_name: z.string().optional(), - }) - .optional(), - otel: z.boolean().optional(), - custom_auth: z.string().optional(), - max_parallel_requests: z.number().optional(), - infer_model_from_keys: z.boolean().optional(), - background_health_checks: z.boolean().optional(), - health_check_interval: z.number().optional(), - alerting: z.array(z.string()).optional(), - alerting_threshold: z.number().optional(), - }), + db_key: z.string().refine( + (key) => key.startsWith('sk-'), // key needed for model management actions + 'Key string must be defined for model management operations, and it must start with "sk-".' + + 'This can be any string, and a random UUID is recommended. Example: sk-f132c7cc-059c-481b-b5ca-a42e191672aa', + ), }); /** @@ -792,7 +673,6 @@ const LiteLLMConfig = z.object({ * @property {string} deploymentStage - Deployment stage for the application. * @property {string} removalPolicy - Removal policy for resources (destroy or retain). * @property {boolean} [runCdkNag=false] - Whether to run CDK Nag checks. - * @property {lambdaConfigSchema} lambdaConfig - Lambda configuration. * @property {string} [lambdaSourcePath='./lambda'] - Path to Lambda source code dir. * @property {string} s3BucketModels - S3 bucket for models. * @property {string} mountS3DebUrl - URL for S3-mounted Debian package. @@ -802,7 +682,6 @@ const LiteLLMConfig = z.object({ * @property {boolean} [deployUi=true] - Whether to deploy UI stacks. * @property {string} logLevel - Log level for application. * @property {AuthConfigSchema} authConfig - Authorization configuration. - * @property {FastApiContainerConfigSchema} restApiConfig - REST API configuration. * @property {RagRepositoryConfigSchema} ragRepositoryConfig - Rag Repository configuration. * @property {RagFileProcessingConfigSchema} ragFileProcessingConfig - Rag file processing configuration. * @property {EcsModelConfigSchema[]} ecsModels - Array of ECS model configurations. @@ -823,7 +702,7 @@ const RawConfigSchema = z .optional() .nullable() .transform((value) => value ?? ''), - deploymentName: z.string(), + deploymentName: z.string().default('prod'), accountNumber: z .number() .or(z.string()) @@ -832,10 +711,11 @@ const RawConfigSchema = z message: 'AWS account number should be 12 digits. If your account ID starts with 0, then please surround the ID with quotation marks.', }), region: z.string(), + restApiConfig: FastApiContainerConfigSchema, vpcId: z.string().optional(), subnetIds: z.array(z.string().startsWith('subnet-')).optional(), - deploymentStage: z.string(), - removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]).transform((value) => REMOVAL_POLICIES[value]), + deploymentStage: z.string().default('prod'), + removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]).transform((value) => REMOVAL_POLICIES[value]).default('destroy'), runCdkNag: z.boolean().default(false), privateEndpoints: z.boolean().optional().default(false), s3BucketModels: z.string(), @@ -847,12 +727,10 @@ const RawConfigSchema = z message: 'AWS account number should be 12 digits. If your account ID starts with 0, then please surround the ID with quotation marks.', }) .optional(), - deployRag: z.boolean().optional().default(false), + deployRag: z.boolean().optional().default(true), deployChat: z.boolean().optional().default(true), deployUi: z.boolean().optional().default(true), - logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]), - lambdaConfig: lambdaConfigSchema, - lambdaSourcePath: z.string().optional().default('./lambda'), + logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]).default('DEBUG'), authConfig: AuthConfigSchema.optional(), pypiConfig: PypiConfigSchema.optional().default({ indexUrl: '', @@ -862,7 +740,6 @@ const RawConfigSchema = z certificateAuthorityBundle: z.string().optional().default(''), ragRepositories: z.array(RagRepositoryConfigSchema).default([]), ragFileProcessingConfig: RagFileProcessingConfigSchema.optional(), - restApiConfig: FastApiContainerConfigSchema, ecsModels: z.array(EcsModelConfigSchema).optional(), apiGatewayConfig: ApiGatewayConfigSchema.optional(), nvmeHostMountPath: z.string().default('/nvme'), @@ -918,7 +795,7 @@ const RawConfigSchema = z .refine( (config) => { return ( - !(config.deployChat || config.deployRag || config.deployUi || config.restApiConfig.internetFacing) || + !(config.deployChat || config.deployRag || config.deployUi) || config.authConfig ); }, diff --git a/lib/serve/index.ts b/lib/serve/index.ts index d8cd2bbe..29867f4d 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -56,7 +56,6 @@ export class LisaServeApplicationStack extends Stack { super(scope, id, props); const { config, vpc } = props; - const rdsConfig = config.restApiConfig.rdsConfig; let tokenTable; if (config.restApiConfig.internetFacing) { @@ -79,7 +78,6 @@ export class LisaServeApplicationStack extends Stack { config: config, resourcePath: path.join(HERE, 'rest-api'), securityGroup: vpc.securityGroups.restApiAlbSg, - taskConfig: config.restApiConfig, tokenTable: tokenTable, vpc: vpc, }); @@ -153,12 +151,12 @@ export class LisaServeApplicationStack extends Stack { subNets?.forEach((subnet) => { litellmDbSg.connections.allowFrom( Peer.ipv4(subnet.ipv4CidrBlock), - Port.tcp(rdsConfig.dbPort), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), 'Allow REST API private subnets to communicate with LiteLLM database', ); }); - const username = rdsConfig.username; + const username = config.restApiConfig.rdsConfig.username; const dbCreds = Credentials.fromGeneratedSecret(username); // DB is a Single AZ instance for cost + inability to make non-Aurora multi-AZ cluster in CDK @@ -180,8 +178,8 @@ export class LisaServeApplicationStack extends Stack { username: username, passwordSecretId: litellmDbPasswordSecret.secretName, dbHost: litellmDb.dbInstanceEndpointAddress, - dbName: rdsConfig.dbName, - dbPort: rdsConfig.dbPort, + dbName: config.restApiConfig.rdsConfig.dbName, + dbPort: config.restApiConfig.rdsConfig.dbPort, }), }); litellmDbPasswordSecret.grantRead(restApi.taskRole); diff --git a/lib/serve/rest-api/src/requirements.txt b/lib/serve/rest-api/src/requirements.txt index d533279a..2931cbc8 100644 --- a/lib/serve/rest-api/src/requirements.txt +++ b/lib/serve/rest-api/src/requirements.txt @@ -7,7 +7,7 @@ cryptography==42.0.8 fastapi==0.111.1 fastapi_utils==0.7.0 gunicorn==22.0.0 -litellm[proxy]==1.43.4 +litellm[proxy]==1.50.4 loguru==0.7.2 pydantic==2.8.2 PyJWT==2.9.0 diff --git a/lib/serve/rest-api/src/utils/generate_litellm_config.py b/lib/serve/rest-api/src/utils/generate_litellm_config.py index 4d9f06a6..9e5bec09 100644 --- a/lib/serve/rest-api/src/utils/generate_litellm_config.py +++ b/lib/serve/rest-api/src/utils/generate_litellm_config.py @@ -49,7 +49,7 @@ def generate_config(filepath: str) -> None: } for model in registered_models ] - config_models = config_contents["model_list"] or [] # ensure config_models is a list and not None + config_models = [] # ensure config_models is a list and not None config_models.extend(litellm_model_params) config_contents["model_list"] = config_models config_contents["litellm_settings"] = { @@ -67,11 +67,13 @@ def generate_config(filepath: str) -> None: f"/{db_params['dbName']}" ) - general_settings = config_contents["general_settings"] - general_settings.update( + config_contents.update( { - "store_model_in_db": True, - "database_url": connection_str, + "general_settings": { + "store_model_in_db": True, + "database_url": connection_str, + "master_key": config_contents["db_key"], + } } ) diff --git a/lib/user-interface/index.ts b/lib/user-interface/index.ts index 2c3c1944..83b7c9fd 100644 --- a/lib/user-interface/index.ts +++ b/lib/user-interface/index.ts @@ -161,8 +161,6 @@ export class UserInterfaceStack extends Stack { }, ); - const litellmModels = config.litellmConfig.model_list ? config.litellmConfig.model_list : []; - // Website bucket deployment // Copy auth and LISA-Serve info to UI deployment bucket @@ -177,7 +175,7 @@ export class UserInterfaceStack extends Stack { createCdkId(['LisaRestApiUri', 'StringParameter']), `${config.deploymentPrefix}/lisaServeRestApiUri`, ).stringValue, - RESTAPI_VERSION: config.restApiConfig.apiVersion, + RESTAPI_VERSION: 'v2', RAG_ENABLED: config.deployRag, SYSTEM_BANNER: { text: config.systemBanner?.text, @@ -185,7 +183,6 @@ export class UserInterfaceStack extends Stack { fontColor: config.systemBanner?.fontColor, }, API_BASE_URL: config.apiGatewayConfig?.domainName ? '/' : `/${config.deploymentStage}/`, - MODELS: litellmModels, }; const appEnvSource = Source.data('env.js', `window.env = ${JSON.stringify(appEnvConfig)}`); diff --git a/lib/user-interface/react/src/components/utils.ts b/lib/user-interface/react/src/components/utils.ts index fea0b96a..3dd1a060 100644 --- a/lib/user-interface/react/src/components/utils.ts +++ b/lib/user-interface/react/src/components/utils.ts @@ -20,9 +20,7 @@ import { PutSessionRequestBody, LisaChatMessage, Repository, - ModelTypes, Model, - DescribeModelsResponseBody, } from './types'; const stripTrailingSlash = (str) => { @@ -167,32 +165,6 @@ export const deleteUserSessions = async (idToken: string) => { return await resp.json(); }; -/** - * Describes all models of a given type which are available to a user - * @param modelType model type we are requesting - * @returns - */ -export const describeModels = async (idToken: string, modelType: ModelTypes): Promise => { - const resp = await sendAuthenticatedRequest(`${RESTAPI_URI}/${RESTAPI_VERSION}/serve/models`, 'GET', idToken); - const modelResponse = (await resp.json()) as DescribeModelsResponseBody; - - return modelResponse.data - .filter((openAiModel) => { - const configModelMatch = window.env.MODELS.filter((configModel) => configModel.model === openAiModel.id)[0]; - if (!configModelMatch || configModelMatch.modelType === modelType) { - return true; - } - }) - .map((openAiModel) => { - const configModelMatch = window.env.MODELS.filter((configModel) => configModel.model === openAiModel.id)[0]; - return { - id: openAiModel.id, - streaming: configModelMatch?.streaming, - modelType: configModelMatch?.modelType, - }; - }); -}; - /** * Returns true or false based on the model health status * @param idToken the user's ID token from authenticating diff --git a/lib/user-interface/react/src/main.tsx b/lib/user-interface/react/src/main.tsx index 039ff81d..e5cc0c4e 100644 --- a/lib/user-interface/react/src/main.tsx +++ b/lib/user-interface/react/src/main.tsx @@ -21,7 +21,6 @@ import './index.css'; import AppConfigured from './components/app-configured'; import '@cloudscape-design/global-styles/index.css'; -import { ModelTypes } from './components/types'; import getStore from './config/store'; declare global { @@ -42,13 +41,6 @@ declare global { backgroundColor: string; fontColor: string; }; - MODELS: [ - { - model: string; - streaming: boolean | null; - modelType: ModelTypes; - }, - ]; }; gitInfo?: { revisionTag?: string; diff --git a/package-lock.json b/package-lock.json index 7801f45a..b5881842 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cdk-nag": "^2.27.198", "constructs": "^10.0.0", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "source-map-support": "^0.5.21", "zod": "^3.22.3" }, @@ -26,6 +27,7 @@ "@stylistic/eslint-plugin": "^2.7.2", "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.5", + "@types/lodash": "^4.17.12", "@types/node": "20.5.3", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.6.0", @@ -3667,6 +3669,13 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.12.tgz", + "integrity": "sha512-sviUmCE8AYdaF/KIHLDJBQgeYzPBI0vf/17NaYehBJfYD1j6/L95Slh07NlyK2iNyBNaEkb3En2jRt+a8y3xZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -9544,8 +9553,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", diff --git a/package.json b/package.json index 2bad7510..a06ca857 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "watch": "tsc -w", "test": "jest", "cdk": "cdk", - "prepare": "husky install" + "prepare": "husky install", + "migrate-properties": "node ./scripts/migrate-properties.mjs" }, "devDependencies": { "@aws-cdk/aws-lambda-python-alpha": "2.125.0-alpha.0", @@ -18,6 +19,7 @@ "@stylistic/eslint-plugin": "^2.7.2", "@types/jest": "^29.5.12", "@types/js-yaml": "^4.0.5", + "@types/lodash": "^4.17.12", "@types/node": "20.5.3", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.6.0", @@ -39,6 +41,7 @@ "cdk-nag": "^2.27.198", "constructs": "^10.0.0", "js-yaml": "^4.1.0", + "lodash": "^4.17.21", "source-map-support": "^0.5.21", "zod": "^3.22.3" }, diff --git a/scripts/migrate-properties.mjs b/scripts/migrate-properties.mjs new file mode 100644 index 00000000..0d6d1c73 --- /dev/null +++ b/scripts/migrate-properties.mjs @@ -0,0 +1,60 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 * as yaml from 'js-yaml'; +import fs from 'fs'; +import path from 'path'; +import _ from 'lodash'; + +console.log('MIGRATING PROPERTIES...'); + +const configFilePath = path.join('./config.yaml'); +const configFile = yaml.load(fs.readFileSync(configFilePath, 'utf8')); + +console.log('FOUND CONFIG FILE: config.yaml\n') + +for (const key in configFile){ + if(_.isPlainObject(configFile[key])) { + const oldConfig = configFile[key] + let newConfig = {...configFile[key]}; + + delete newConfig.lambdaConfig; + delete newConfig.litellmConfig; + delete newConfig.restApiConfig; + + newConfig['restApiConfig'] = { + 'sslCertIamArn': oldConfig['restApiConfig']['loadBalancerConfig']['sslCertIamArn'], + 'internetFacing': oldConfig['restApiConfig']['internetFacing'], + 'domainName': oldConfig['restApiConfig']['loadBalancerConfig']['domainName'], + 'rdsConfig': oldConfig['restApiConfig']['rdsConfig'], + } + + newConfig['litellmConfig'] = { + 'dbKey': oldConfig['litellmConfig']['general_settings']['master_key'] + } + + if (JSON.stringify(newConfig.restApiConfig) === '{}'){ + delete newConfig.restApiConfig; + } + + if (JSON.stringify(newConfig.litellmConfig) === '{}'){ + delete newConfig.litellmConfig; + } + + console.log('NEW CONFIG FILE = \n' + yaml.dump(_(newConfig).omit(_.isNil).value())); + fs.writeFileSync('./config-custom.yaml', yaml.dump(_(newConfig).omit(_.isNil).value())); + } +} diff --git a/test/cdk/mocks/config.yaml b/test/cdk/mocks/config.yaml index b364623e..059f97f7 100644 --- a/test/cdk/mocks/config.yaml +++ b/test/cdk/mocks/config.yaml @@ -35,57 +35,12 @@ dev: accountNumbersEcr: - '012345678901' deployRag: true - lambdaConfig: - pythonRuntime: PYTHON_3_10 - logLevel: DEBUG - vpcAutoscalingConfig: - provisionedConcurrentExecutions: 5 - minCapacity: 1 - maxCapacity: 50 - targetValue: 0.80 - cooldown: 30 authConfig: authority: test clientId: test logLevel: DEBUG - # NOTE: The following configuration will allow for using a custom domain for the chat user interface. - # If this option is specified, the API Gateway invocation URL will NOT work on its own as the application URL. - # Users must use the custom domain for the user interface to work if this option is populated. - apiGatewayConfig: - domainName: restApiConfig: - apiVersion: v2 - instanceType: m5.large - containerConfig: - image: - baseImage: python:3.9 - path: lib/serve/rest-api - type: asset - healthCheckConfig: - command: ["CMD-SHELL", "exit 0"] - interval: 10 - startPeriod: 30 - timeout: 5 - retries: 3 - autoScalingConfig: - minCapacity: 1 - maxCapacity: 1 - cooldown: 60 - defaultInstanceWarmup: 60 - metricConfig: - AlbMetricName: RequestCountPerTarget - targetValue: 1000 - duration: 60 - estimatedInstanceWarmup: 30 - loadBalancerConfig: - sslCertIamArn: arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev - healthCheckConfig: - path: /health - interval: 60 - timeout: 30 - healthyThresholdCount: 2 - unhealthyThresholdCount: 10 - domainName: + sslCertIamArn: arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev ragRepositories: - repositoryId: pgvector-rag type: pgvector @@ -125,8 +80,4 @@ dev: # inferenceContainer: tgi # baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 litellmConfig: - general_settings: - master_key: sk-012345 - litellm_settings: - telemetry: false - model_list: + db_key: sk-012345 #pragma: allowlist secret From 37ec430202d16957f86b0ed48b3bf2832a653319 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 29 Oct 2024 11:11:29 -0600 Subject: [PATCH 14/48] Deploy to dev acct --- .github/workflows/code.deploy.dev.yml | 0 Makefile | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/code.deploy.dev.yml diff --git a/.github/workflows/code.deploy.dev.yml b/.github/workflows/code.deploy.dev.yml new file mode 100644 index 00000000..e69de29b diff --git a/Makefile b/Makefile index c478e825..c5129a00 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ DEPLOYMENT_NAME := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .deploymentN endif ifeq (${DEPLOYMENT_NAME}, null) -$(error deploymentName must be set in command line using DEPLOYMENT_NAME variable or config files) +DEPLOYMENT_NAME := prod endif # ACCOUNT_NUMBER @@ -81,7 +81,7 @@ DEPLOYMENT_STAGE := $(shell cat $(PROJECT_DIR)/config-base.yaml | yq .deployment endif ifeq (${DEPLOYMENT_STAGE}, null) -$(error deploymentStage must be set in config files) +DEPLOYMENT_STAGE := prod endif # ACCOUNT_NUMBERS_ECR - AWS account numbers that need to be logged into with Docker CLI to use ECR From 89cc70b83bb51ae90ac99cc5837e902adab4963d Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 29 Oct 2024 11:12:50 -0600 Subject: [PATCH 15/48] Deploy to dev acct --- .github/workflows/code.deploy.dev.yml | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/code.deploy.dev.yml b/.github/workflows/code.deploy.dev.yml index e69de29b..e950fff0 100644 --- a/.github/workflows/code.deploy.dev.yml +++ b/.github/workflows/code.deploy.dev.yml @@ -0,0 +1,65 @@ +name: Deploy Dev Environment +on: + push: + branches: [ "main", "develop", "release/**" ] + +permissions: + id-token: write + contents: read + +jobs: + CheckPendingWorkflow: + runs-on: ubuntu-latest + steps: + - uses: ahmadnassri/action-workflow-queue@v1 + with: + delay: 300000 + timeout: 7200000 + DeployLISA: + needs: CheckPendingWorkflow + environment: development + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT }}:role/${{ vars.ROLE_NAME_TO_ASSUME }} + role-session-name: GitHub_to_AWS_via_FederatedOIDC + role-duration-seconds: 7200 + - name: Create config-custom.yaml + id: create-yaml + run: | + echo ${{vars.CONFIG_YAML}} > config-custom.yaml + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Install CDK dependencies + run: | + npm install + - name: Deploy LISA + run: | + make deploy HEADLESS=true + SendSlackNotification: + name: Send Slack Notification + needs: [ DeployLISA ] + runs-on: ubuntu-latest + if: always() + steps: + - name: Send Notification that Dev Deploy Finished + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_COLOR: ${{ contains(join(needs.*.result, ' '), 'failure') && 'failure' || 'success' }} + SLACK_TITLE: 'Dev Deploy Finished' + SLACK_FOOTER: '' + MSG_MINIMAL: 'actions url,commit' + SLACK_MESSAGE_ON_FAILURE: ' Dev Deploy FAILED on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>' + SLACK_MESSAGE_ON_SUCCESS: 'Dev Deploy SUCCESS on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>.' + SLACK_MESSAGE: 'Dev Deploy Finished with status ${{ job.status }} on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>' From d55338b879393ddedc035b250b5180ac23c48d8d Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 29 Oct 2024 11:12:50 -0600 Subject: [PATCH 16/48] Deploy to dev acct --- .github/workflows/code.deploy.dev.yml | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/code.deploy.dev.yml b/.github/workflows/code.deploy.dev.yml index e69de29b..abd36b55 100644 --- a/.github/workflows/code.deploy.dev.yml +++ b/.github/workflows/code.deploy.dev.yml @@ -0,0 +1,65 @@ +name: Deploy Dev Environment +on: + push: + branches: [ "main", "develop", "release/**" ] + +permissions: + id-token: write + contents: read + +jobs: + CheckPendingWorkflow: + runs-on: ubuntu-latest + steps: + - uses: ahmadnassri/action-workflow-queue@v1 + with: + delay: 300000 + timeout: 7200000 + DeployLISA: + needs: CheckPendingWorkflow + environment: dev + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT }}:role/${{ vars.ROLE_NAME_TO_ASSUME }} + role-session-name: GitHub_to_AWS_via_FederatedOIDC + role-duration-seconds: 7200 + - name: Create config-custom.yaml + id: create-yaml + run: | + echo ${{vars.CONFIG_YAML}} > config-custom.yaml + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Install CDK dependencies + run: | + npm install + - name: Deploy LISA + run: | + make deploy HEADLESS=true + SendSlackNotification: + name: Send Slack Notification + needs: [ DeployLISA ] + runs-on: ubuntu-latest + if: always() + steps: + - name: Send Notification that Dev Deploy Finished + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} + SLACK_COLOR: ${{ contains(join(needs.*.result, ' '), 'failure') && 'failure' || 'success' }} + SLACK_TITLE: 'Dev Deploy Finished' + SLACK_FOOTER: '' + MSG_MINIMAL: 'actions url,commit' + SLACK_MESSAGE_ON_FAILURE: ' Dev Deploy FAILED on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>' + SLACK_MESSAGE_ON_SUCCESS: 'Dev Deploy SUCCESS on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>.' + SLACK_MESSAGE: 'Dev Deploy Finished with status ${{ job.status }} on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>' From 681af475a903a8e294dc9874871b4ec52d28c5ff Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 29 Oct 2024 11:42:15 -0600 Subject: [PATCH 17/48] Update code.deploy.dev.yml --- .github/workflows/code.deploy.dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code.deploy.dev.yml b/.github/workflows/code.deploy.dev.yml index abd36b55..6c5c9ead 100644 --- a/.github/workflows/code.deploy.dev.yml +++ b/.github/workflows/code.deploy.dev.yml @@ -31,7 +31,7 @@ jobs: - name: Create config-custom.yaml id: create-yaml run: | - echo ${{vars.CONFIG_YAML}} > config-custom.yaml + echo "${{vars.CONFIG_YAML}}" > config-custom.yaml - name: Set up Python 3.11 uses: actions/setup-python@v5 with: From c6b0e37e5d1aa096e22db6d64c4bcdf77ea9cec6 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 29 Oct 2024 14:53:37 -0600 Subject: [PATCH 18/48] Support deploying to the demo acct --- .github/workflows/code.deploy.demo.yml | 65 ++++++++++++++++++++++++++ .github/workflows/code.deploy.dev.yml | 2 +- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/code.deploy.demo.yml diff --git a/.github/workflows/code.deploy.demo.yml b/.github/workflows/code.deploy.demo.yml new file mode 100644 index 00000000..cefda522 --- /dev/null +++ b/.github/workflows/code.deploy.demo.yml @@ -0,0 +1,65 @@ +name: Deploy Demo Environment +on: + push: + branches: [ "main"] + +permissions: + id-token: write + contents: read + +jobs: + CheckPendingWorkflow: + runs-on: ubuntu-latest + steps: + - uses: ahmadnassri/action-workflow-queue@v1 + with: + delay: 300000 + timeout: 7200000 + DeployLISA: + needs: CheckPendingWorkflow + environment: demo + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ vars.AWS_REGION }} + role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT }}:role/${{ vars.ROLE_NAME_TO_ASSUME }} + role-session-name: GitHub_to_AWS_via_FederatedOIDC + role-duration-seconds: 14400 + - name: Create config-custom.yaml + id: create-yaml + run: | + echo "${{vars.CONFIG_YAML}}" > config-custom.yaml + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Use Node.js 20.x + uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Install CDK dependencies + run: | + npm install + - name: Deploy LISA + run: | + make deploy HEADLESS=true + SendSlackNotification: + name: Send Slack Notification + needs: [ DeployLISA ] + runs-on: ubuntu-latest + if: always() + steps: + - name: Send Notification that Demo Deploy Finished + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_WEBHOOK: ${{ secrets.INTERNAL_DEV_SLACK_WEBHOOK_URL }} + SLACK_COLOR: ${{ contains(join(needs.*.result, ' '), 'failure') && 'failure' || 'success' }} + SLACK_TITLE: 'Demo Deploy Finished' + SLACK_FOOTER: '' + MSG_MINIMAL: 'actions url,commit' + SLACK_MESSAGE_ON_FAILURE: ' Demo Deploy FAILED on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>' + SLACK_MESSAGE_ON_SUCCESS: 'Demo Deploy SUCCESS on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>.' + SLACK_MESSAGE: 'Demo Deploy Finished with status ${{ job.status }} on branch ${{ github.head_ref || github.ref_name }} for <${{ github.event.pull_request.html_url || github.event.head_commit.url }}|commit>' diff --git a/.github/workflows/code.deploy.dev.yml b/.github/workflows/code.deploy.dev.yml index 6c5c9ead..514e8910 100644 --- a/.github/workflows/code.deploy.dev.yml +++ b/.github/workflows/code.deploy.dev.yml @@ -27,7 +27,7 @@ jobs: aws-region: ${{ vars.AWS_REGION }} role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT }}:role/${{ vars.ROLE_NAME_TO_ASSUME }} role-session-name: GitHub_to_AWS_via_FederatedOIDC - role-duration-seconds: 7200 + role-duration-seconds: 14400 - name: Create config-custom.yaml id: create-yaml run: | From b7e9507968dc796c9515802ef86894ddf49b421d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 22:54:04 -0600 Subject: [PATCH 19/48] Bump langchain from 0.2.16 to 0.3.0 in /lib/rag/layer --- lib/rag/layer/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rag/layer/requirements.txt b/lib/rag/layer/requirements.txt index 88694e98..9152ae70 100644 --- a/lib/rag/layer/requirements.txt +++ b/lib/rag/layer/requirements.txt @@ -1,6 +1,6 @@ boto3>=1.34.131 botocore>=1.34.131 -langchain==0.2.16 +langchain==0.3.0 langchain-community==0.2.17 langchain-openai==0.1.25 opensearch-py==2.6.0 From 5ba06ab8099f3dc00b70d0ab37c5244da813efc8 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Wed, 30 Oct 2024 23:08:30 -0600 Subject: [PATCH 20/48] Upgrading per dependabot --- lib/rag/layer/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rag/layer/requirements.txt b/lib/rag/layer/requirements.txt index 9152ae70..d35e937e 100644 --- a/lib/rag/layer/requirements.txt +++ b/lib/rag/layer/requirements.txt @@ -1,8 +1,8 @@ boto3>=1.34.131 botocore>=1.34.131 langchain==0.3.0 -langchain-community==0.2.17 -langchain-openai==0.1.25 +langchain-community==0.3.0 +langchain-openai==0.2.4 opensearch-py==2.6.0 pgvector==0.2.5 psycopg2-binary==2.9.9 From da583cb871fd17eb108bedd736487eaea63c5555 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:20:49 -0600 Subject: [PATCH 21/48] Bump @langchain/community and langchain in /lib/user-interface/react --- lib/user-interface/react/package-lock.json | 885 ++++----------------- lib/user-interface/react/package.json | 4 +- 2 files changed, 138 insertions(+), 751 deletions(-) diff --git a/lib/user-interface/react/package-lock.json b/lib/user-interface/react/package-lock.json index 3fd22531..5e54f4b7 100644 --- a/lib/user-interface/react/package-lock.json +++ b/lib/user-interface/react/package-lock.json @@ -17,7 +17,7 @@ "@reduxjs/toolkit": "^1.9.5", "axios": "^1.7.4", "git-repo-info": "^2.1.1", - "langchain": "^0.1.12", + "langchain": "^0.3.5", "lodash": "^4.17.21", "luxon": "^3.4.0", "react": "^18.2.0", @@ -77,30 +77,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.9.1.tgz", - "integrity": "sha512-wa1meQ2WSfoY8Uor3EdrJq0jTiZJoKoSii2ZVWRY1oN4Tlr5s59pADg9T79FTbPe1/se5c3pBeZgJL63wmuoBA==", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" - } - }, - "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { - "version": "18.19.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", - "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@babel/runtime": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", @@ -782,350 +758,6 @@ "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, - "node_modules/@langchain/community": { - "version": "0.0.26", - "resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.0.26.tgz", - "integrity": "sha512-E5/lltEkkRCxA9WQ/IpdTWUBj5gaCOYuf6r2MX4ZNTR5gfaZkHdLQWF1rew6uG3Z7XjRMMtIxxT9jS7me6sRRA==", - "dependencies": { - "@langchain/core": "~0.1.16", - "@langchain/openai": "~0.0.10", - "flat": "^5.0.2", - "langsmith": "~0.0.48", - "uuid": "^9.0.0", - "zod": "^3.22.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@aws-crypto/sha256-js": "^5.0.0", - "@aws-sdk/client-bedrock-agent-runtime": "^3.485.0", - "@aws-sdk/client-bedrock-runtime": "^3.422.0", - "@aws-sdk/client-dynamodb": "^3.310.0", - "@aws-sdk/client-kendra": "^3.352.0", - "@aws-sdk/client-lambda": "^3.310.0", - "@aws-sdk/client-sagemaker-runtime": "^3.310.0", - "@aws-sdk/client-sfn": "^3.310.0", - "@aws-sdk/credential-provider-node": "^3.388.0", - "@azure/search-documents": "^12.0.0", - "@clickhouse/client": "^0.2.5", - "@cloudflare/ai": "*", - "@datastax/astra-db-ts": "^0.1.4", - "@elastic/elasticsearch": "^8.4.0", - "@getmetal/metal-sdk": "*", - "@getzep/zep-js": "^0.9.0", - "@gomomento/sdk": "^1.51.1", - "@gomomento/sdk-core": "^1.51.1", - "@google-ai/generativelanguage": "^0.2.1", - "@gradientai/nodejs-sdk": "^1.2.0", - "@huggingface/inference": "^2.6.4", - "@mozilla/readability": "*", - "@opensearch-project/opensearch": "*", - "@pinecone-database/pinecone": "*", - "@planetscale/database": "^1.8.0", - "@qdrant/js-client-rest": "^1.2.0", - "@raycast/api": "^1.55.2", - "@rockset/client": "^0.9.1", - "@smithy/eventstream-codec": "^2.0.5", - "@smithy/protocol-http": "^3.0.6", - "@smithy/signature-v4": "^2.0.10", - "@smithy/util-utf8": "^2.0.0", - "@supabase/postgrest-js": "^1.1.1", - "@supabase/supabase-js": "^2.10.0", - "@tensorflow-models/universal-sentence-encoder": "*", - "@tensorflow/tfjs-converter": "*", - "@tensorflow/tfjs-core": "*", - "@upstash/redis": "^1.20.6", - "@vercel/kv": "^0.2.3", - "@vercel/postgres": "^0.5.0", - "@writerai/writer-sdk": "^0.40.2", - "@xata.io/client": "^0.28.0", - "@xenova/transformers": "^2.5.4", - "@zilliz/milvus2-sdk-node": ">=2.2.7", - "cassandra-driver": "^4.7.2", - "chromadb": "*", - "closevector-common": "0.1.0-alpha.1", - "closevector-node": "0.1.0-alpha.10", - "closevector-web": "0.1.0-alpha.16", - "cohere-ai": "*", - "convex": "^1.3.1", - "discord.js": "^14.14.1", - "faiss-node": "^0.5.1", - "firebase-admin": "^11.9.0", - "google-auth-library": "^8.9.0", - "googleapis": "^126.0.1", - "hnswlib-node": "^1.4.2", - "html-to-text": "^9.0.5", - "ioredis": "^5.3.2", - "jsdom": "*", - "llmonitor": "^0.5.9", - "lodash": "^4.17.21", - "lunary": "^0.6.11", - "mongodb": "^5.2.0", - "mysql2": "^3.3.3", - "neo4j-driver": "*", - "node-llama-cpp": "*", - "pg": "^8.11.0", - "pg-copy-streams": "^6.0.5", - "pickleparser": "^0.2.1", - "portkey-ai": "^0.1.11", - "redis": "^4.6.4", - "replicate": "^0.18.0", - "typeorm": "^0.3.12", - "typesense": "^1.5.3", - "usearch": "^1.1.1", - "vectordb": "^0.1.4", - "voy-search": "0.6.2", - "weaviate-ts-client": "^1.4.0", - "web-auth-library": "^1.0.3", - "ws": "^8.14.2" - }, - "peerDependenciesMeta": { - "@aws-crypto/sha256-js": { - "optional": true - }, - "@aws-sdk/client-bedrock-agent-runtime": { - "optional": true - }, - "@aws-sdk/client-bedrock-runtime": { - "optional": true - }, - "@aws-sdk/client-dynamodb": { - "optional": true - }, - "@aws-sdk/client-kendra": { - "optional": true - }, - "@aws-sdk/client-lambda": { - "optional": true - }, - "@aws-sdk/client-sagemaker-runtime": { - "optional": true - }, - "@aws-sdk/client-sfn": { - "optional": true - }, - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@azure/search-documents": { - "optional": true - }, - "@clickhouse/client": { - "optional": true - }, - "@cloudflare/ai": { - "optional": true - }, - "@datastax/astra-db-ts": { - "optional": true - }, - "@elastic/elasticsearch": { - "optional": true - }, - "@getmetal/metal-sdk": { - "optional": true - }, - "@getzep/zep-js": { - "optional": true - }, - "@gomomento/sdk": { - "optional": true - }, - "@gomomento/sdk-core": { - "optional": true - }, - "@google-ai/generativelanguage": { - "optional": true - }, - "@gradientai/nodejs-sdk": { - "optional": true - }, - "@huggingface/inference": { - "optional": true - }, - "@mozilla/readability": { - "optional": true - }, - "@opensearch-project/opensearch": { - "optional": true - }, - "@pinecone-database/pinecone": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@qdrant/js-client-rest": { - "optional": true - }, - "@raycast/api": { - "optional": true - }, - "@rockset/client": { - "optional": true - }, - "@smithy/eventstream-codec": { - "optional": true - }, - "@smithy/protocol-http": { - "optional": true - }, - "@smithy/signature-v4": { - "optional": true - }, - "@smithy/util-utf8": { - "optional": true - }, - "@supabase/postgrest-js": { - "optional": true - }, - "@supabase/supabase-js": { - "optional": true - }, - "@tensorflow-models/universal-sentence-encoder": { - "optional": true - }, - "@tensorflow/tfjs-converter": { - "optional": true - }, - "@tensorflow/tfjs-core": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "@vercel/postgres": { - "optional": true - }, - "@writerai/writer-sdk": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "@xenova/transformers": { - "optional": true - }, - "@zilliz/milvus2-sdk-node": { - "optional": true - }, - "cassandra-driver": { - "optional": true - }, - "chromadb": { - "optional": true - }, - "closevector-common": { - "optional": true - }, - "closevector-node": { - "optional": true - }, - "closevector-web": { - "optional": true - }, - "cohere-ai": { - "optional": true - }, - "convex": { - "optional": true - }, - "discord.js": { - "optional": true - }, - "faiss-node": { - "optional": true - }, - "firebase-admin": { - "optional": true - }, - "google-auth-library": { - "optional": true - }, - "googleapis": { - "optional": true - }, - "hnswlib-node": { - "optional": true - }, - "html-to-text": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "llmonitor": { - "optional": true - }, - "lodash": { - "optional": true - }, - "lunary": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "neo4j-driver": { - "optional": true - }, - "node-llama-cpp": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-copy-streams": { - "optional": true - }, - "pickleparser": { - "optional": true - }, - "portkey-ai": { - "optional": true - }, - "redis": { - "optional": true - }, - "replicate": { - "optional": true - }, - "typeorm": { - "optional": true - }, - "typesense": { - "optional": true - }, - "usearch": { - "optional": true - }, - "vectordb": { - "optional": true - }, - "voy-search": { - "optional": true - }, - "weaviate-ts-client": { - "optional": true - }, - "web-auth-library": { - "optional": true - }, - "ws": { - "optional": true - } - } - }, "node_modules/@langchain/core": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.1.22.tgz", @@ -1169,21 +801,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@langchain/openai": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.0.14.tgz", - "integrity": "sha512-co6nRylPrLGY/C3JYxhHt6cxLq07P086O7K3QaZH7SFFErIN9wSzJonpvhZR07DEUq6eK6wKgh2ORxA/NcjSRQ==", - "dependencies": { - "@langchain/core": "~0.1.13", - "js-tiktoken": "^1.0.7", - "openai": "^4.26.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@microsoft/fetch-event-source": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", @@ -2130,11 +1747,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2338,14 +1950,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -2429,6 +2033,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2448,14 +2060,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", @@ -2653,15 +2257,6 @@ "node": ">=0.3.1" } }, - "node_modules/digest-fetch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", - "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", - "dependencies": { - "base-64": "^0.1.0", - "md5": "^2.3.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3202,11 +2797,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/expr-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz", - "integrity": "sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==" - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3263,29 +2853,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" - } - ], - "optional": true, - "peer": true, - "dependencies": { - "strnum": "^1.0.5" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -3338,14 +2905,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -3423,14 +2982,6 @@ "node": ">= 12.20" } }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "engines": { - "node": ">= 14" - } - }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -3764,7 +3315,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 4" } @@ -4218,9 +3769,9 @@ } }, "node_modules/js-tiktoken": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.10.tgz", - "integrity": "sha512-ZoSxbGjvGyMT13x6ACo9ebhDha/0FHdKA+OsQcMOWcm1Zs7r90Rhk5lhERLzji+3rA7EKpXCgwXcM5fF3DMpdA==", + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.15.tgz", + "integrity": "sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==", "dependencies": { "base64-js": "^1.5.1" } @@ -4280,25 +3831,19 @@ "peer": true }, "node_modules/langchain": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.1.12.tgz", - "integrity": "sha512-F3WK6KJGeA+gnXIrijKy892yEGzUOpO4pEWWphUrCxrtfjXh1hFcXfj5Oh14qGvaUCmn8ezBqQMJ/LhL6z3DhQ==", - "dependencies": { - "@anthropic-ai/sdk": "^0.9.1", - "@langchain/community": "~0.0.20", - "@langchain/core": "~0.1.16", - "@langchain/openai": "~0.0.12", - "binary-extensions": "^2.2.0", - "expr-eval": "^2.0.2", - "js-tiktoken": "^1.0.7", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.5.tgz", + "integrity": "sha512-Gq0xC45Sq6nszS8kQG9suCrmBsuXH0INMmiF7D2TwPb6mtG35Jiq4grCk9ykpwPsarTHdty3SzUbII/FqiYSSw==", + "dependencies": { + "@langchain/openai": ">=0.1.0 <0.4.0", + "@langchain/textsplitters": ">=0.0.0 <0.2.0", + "js-tiktoken": "^1.0.12", "js-yaml": "^4.1.0", "jsonpointer": "^5.0.1", - "langchainhub": "~0.0.6", - "langsmith": "~0.0.59", - "ml-distance": "^4.0.0", + "langsmith": "^0.2.0", "openapi-types": "^12.1.3", "p-retry": "4", - "uuid": "^9.0.0", + "uuid": "^10.0.0", "yaml": "^2.2.1", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" @@ -4307,108 +3852,44 @@ "node": ">=18" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.310.0", - "@aws-sdk/client-sagemaker-runtime": "^3.310.0", - "@aws-sdk/client-sfn": "^3.310.0", - "@aws-sdk/credential-provider-node": "^3.388.0", - "@azure/storage-blob": "^12.15.0", - "@gomomento/sdk": "^1.51.1", - "@gomomento/sdk-core": "^1.51.1", - "@gomomento/sdk-web": "^1.51.1", - "@google-ai/generativelanguage": "^0.2.1", - "@google-cloud/storage": "^6.10.1", - "@notionhq/client": "^2.2.10", - "@pinecone-database/pinecone": "*", - "@supabase/supabase-js": "^2.10.0", - "@vercel/kv": "^0.2.3", - "@xata.io/client": "^0.28.0", - "apify-client": "^2.7.1", - "assemblyai": "^4.0.0", + "@langchain/anthropic": "*", + "@langchain/aws": "*", + "@langchain/cohere": "*", + "@langchain/core": ">=0.2.21 <0.4.0", + "@langchain/google-genai": "*", + "@langchain/google-vertexai": "*", + "@langchain/groq": "*", + "@langchain/mistralai": "*", + "@langchain/ollama": "*", "axios": "*", - "cheerio": "^1.0.0-rc.12", - "chromadb": "*", - "convex": "^1.3.1", - "d3-dsv": "^2.0.0", - "epub2": "^3.0.1", - "fast-xml-parser": "^4.2.7", - "google-auth-library": "^8.9.0", - "googleapis": "^126.0.1", + "cheerio": "*", "handlebars": "^4.7.8", - "html-to-text": "^9.0.5", - "ignore": "^5.2.0", - "ioredis": "^5.3.2", - "jsdom": "*", - "mammoth": "^1.6.0", - "mongodb": "^5.2.0", - "node-llama-cpp": "*", - "notion-to-md": "^3.1.0", - "officeparser": "^4.0.4", - "pdf-parse": "1.1.1", "peggy": "^3.0.2", - "playwright": "^1.32.1", - "puppeteer": "^19.7.2", - "pyodide": "^0.24.1", - "redis": "^4.6.4", - "sonix-speech-recognition": "^2.1.1", - "srt-parser-2": "^1.2.3", - "typeorm": "^0.3.12", - "vectordb": "^0.1.4", - "weaviate-ts-client": "^1.4.0", - "web-auth-library": "^1.0.3", - "ws": "^8.14.2", - "youtube-transcript": "^1.0.6", - "youtubei.js": "^5.8.0" + "typeorm": "*" }, "peerDependenciesMeta": { - "@aws-sdk/client-s3": { - "optional": true - }, - "@aws-sdk/client-sagemaker-runtime": { - "optional": true - }, - "@aws-sdk/client-sfn": { - "optional": true - }, - "@aws-sdk/credential-provider-node": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@gomomento/sdk": { - "optional": true - }, - "@gomomento/sdk-core": { + "@langchain/anthropic": { "optional": true }, - "@gomomento/sdk-web": { + "@langchain/aws": { "optional": true }, - "@google-ai/generativelanguage": { + "@langchain/cohere": { "optional": true }, - "@google-cloud/storage": { + "@langchain/google-genai": { "optional": true }, - "@notionhq/client": { + "@langchain/google-vertexai": { "optional": true }, - "@pinecone-database/pinecone": { + "@langchain/groq": { "optional": true }, - "@supabase/supabase-js": { + "@langchain/mistralai": { "optional": true }, - "@vercel/kv": { - "optional": true - }, - "@xata.io/client": { - "optional": true - }, - "apify-client": { - "optional": true - }, - "assemblyai": { + "@langchain/ollama": { "optional": true }, "axios": { @@ -4417,111 +3898,85 @@ "cheerio": { "optional": true }, - "chromadb": { - "optional": true - }, - "convex": { - "optional": true - }, - "d3-dsv": { - "optional": true - }, - "epub2": { - "optional": true - }, - "faiss-node": { - "optional": true - }, - "fast-xml-parser": { - "optional": true - }, - "google-auth-library": { - "optional": true - }, - "googleapis": { - "optional": true - }, "handlebars": { "optional": true }, - "html-to-text": { - "optional": true - }, - "ignore": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "mammoth": { - "optional": true - }, - "mongodb": { - "optional": true - }, - "node-llama-cpp": { - "optional": true - }, - "notion-to-md": { - "optional": true - }, - "officeparser": { - "optional": true - }, - "pdf-parse": { - "optional": true - }, "peggy": { "optional": true }, - "playwright": { - "optional": true - }, - "puppeteer": { - "optional": true - }, - "pyodide": { - "optional": true - }, - "redis": { - "optional": true - }, - "sonix-speech-recognition": { - "optional": true - }, - "srt-parser-2": { - "optional": true - }, "typeorm": { "optional": true - }, - "vectordb": { - "optional": true - }, - "weaviate-ts-client": { - "optional": true - }, - "web-auth-library": { - "optional": true - }, - "ws": { - "optional": true - }, - "youtube-transcript": { - "optional": true - }, - "youtubei.js": { + } + } + }, + "node_modules/langchain/node_modules/@langchain/openai": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.11.tgz", + "integrity": "sha512-mEFbpJ8w8NPArsquUlCwxvZTKNkXxqwzvTEYzv6Jb7gUoBDOZtwLg6AdcngTJ+w5VFh3wxgPy0g3zb9Aw0Qbpw==", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^4.68.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.26 <0.4.0" + } + }, + "node_modules/langchain/node_modules/@langchain/textsplitters": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", + "integrity": "sha512-djI4uw9rlkAb5iMhtLED+xJebDdAG935AdP4eRTB02R7OB/act55Bj9wsskhZsvuyQRpO4O1wQOp85s6T6GWmw==", + "dependencies": { + "js-tiktoken": "^1.0.12" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.21 <0.4.0" + } + }, + "node_modules/langchain/node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" + }, + "node_modules/langchain/node_modules/langsmith": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.2.3.tgz", + "integrity": "sha512-SPMYPVqR9kwXZVmJ2PXC61HeBnXIFHrjfjDxQ14H0+n5p4gqjLzgSHIQyxBlFeWQUQzArJxe65Ap+s+Xo1cZog==", + "dependencies": { + "@types/uuid": "^10.0.0", + "commander": "^10.0.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "openai": "*" + }, + "peerDependenciesMeta": { + "openai": { "optional": true } } }, - "node_modules/langchainhub": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/langchainhub/-/langchainhub-0.0.6.tgz", - "integrity": "sha512-SW6105T+YP1cTe0yMf//7kyshCgvCTyFBMTgH2H3s9rTAR4e+78DA/BBrUL/Mt4Q5eMWui7iGuAYb3pgGsdQ9w==" + "node_modules/langchain/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } }, "node_modules/langsmith": { "version": "0.0.66", @@ -4538,14 +3993,6 @@ "langsmith": "dist/cli/main.cjs" } }, - "node_modules/langsmith/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4616,18 +4063,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/luxon": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.0.tgz", @@ -4636,21 +4071,6 @@ "node": ">=12" } }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, - "node_modules/md5/node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/mdast-util-definitions": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", @@ -5334,9 +4754,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -5537,28 +4957,34 @@ } }, "node_modules/openai": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.26.0.tgz", - "integrity": "sha512-HPC7tgYdeP38F3uHA5WgnoXZyGbAp9jgcIo23p6It+q/07u4C+NZ8xHKlMShsPbDDmFRpPsa3vdbXYpbhJH3eg==", + "version": "4.68.4", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.68.4.tgz", + "integrity": "sha512-LRinV8iU9VQplkr25oZlyrsYGPGasIwYN8KFMAAFTHHLHjHhejtJ5BALuLFrkGzY4wfbKhOhuT+7lcHZ+F3iEA==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", - "digest-fetch": "^1.3.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7", - "web-streams-polyfill": "^3.2.1" + "node-fetch": "^2.6.7" }, "bin": { "openai": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", - "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", + "version": "18.19.61", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.61.tgz", + "integrity": "sha512-z8fH66NcVkDzBItOao+Nyh0fiy7CYdxIyxnNCcZ60aY0I+EA/y4TSi/S/W9i8DIQvwVo7a0pgzAxmDeNnqrpkw==", "dependencies": { "undici-types": "~5.26.4" } @@ -6519,13 +5945,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -6714,13 +6136,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "optional": true, - "peer": true - }, "node_modules/style-to-object": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", @@ -7418,11 +6833,11 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", "engines": { - "node": ">= 8" + "node": ">= 14" } }, "node_modules/webidl-conversions": { @@ -7499,34 +6914,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yaml": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", @@ -7548,9 +6935,9 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/lib/user-interface/react/package.json b/lib/user-interface/react/package.json index adeaa260..35fc0026 100644 --- a/lib/user-interface/react/package.json +++ b/lib/user-interface/react/package.json @@ -15,12 +15,12 @@ "@cloudscape-design/component-toolkit": "^1.0.0-beta.65", "@cloudscape-design/components": "^3.0.638", "@cloudscape-design/global-styles": "^1.0.12", - "@langchain/core": "^0.1.22", + "@langchain/core": "^0.3.5", "@microsoft/fetch-event-source": "^2.0.1", "@reduxjs/toolkit": "^1.9.5", "axios": "^1.7.4", "git-repo-info": "^2.1.1", - "langchain": "^0.1.12", + "langchain": "^0.3.5", "lodash": "^4.17.21", "luxon": "^3.4.0", "react": "^18.2.0", From 0fb386d1194e9cbfb719759f1eb2e69f4ce4bdc0 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 31 Oct 2024 00:23:02 -0600 Subject: [PATCH 22/48] adding langchain openai --- lib/user-interface/react/package-lock.json | 180 +++++++++------------ lib/user-interface/react/package.json | 1 + 2 files changed, 74 insertions(+), 107 deletions(-) diff --git a/lib/user-interface/react/package-lock.json b/lib/user-interface/react/package-lock.json index 5e54f4b7..6003ea49 100644 --- a/lib/user-interface/react/package-lock.json +++ b/lib/user-interface/react/package-lock.json @@ -12,7 +12,8 @@ "@cloudscape-design/component-toolkit": "^1.0.0-beta.65", "@cloudscape-design/components": "^3.0.638", "@cloudscape-design/global-styles": "^1.0.12", - "@langchain/core": "^0.1.22", + "@langchain/core": "^0.3.5", + "@langchain/openai": "^0.3.11", "@microsoft/fetch-event-source": "^2.0.1", "@reduxjs/toolkit": "^1.9.5", "axios": "^1.7.4", @@ -759,19 +760,20 @@ "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" }, "node_modules/@langchain/core": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.1.22.tgz", - "integrity": "sha512-I3KMv87D5AFeAvuJhzaGOYdppFL4h/bRm7LeJfwF2PspQIZwvDE9GP7hkw4n+7jwNaBxjU8ZTj6o3LZAh1R5LQ==", + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-0.3.16.tgz", + "integrity": "sha512-g83M2Z1XlhECFUtT4C7XLsVVGt2Hk3Y/KhS5tZSsz+Gqtxwd790/MD7MxdUHpZj0VKkvrFuWARWpJmNKlkiY+g==", + "license": "MIT", "dependencies": { "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", - "js-tiktoken": "^1.0.8", - "langsmith": "~0.0.48", - "ml-distance": "^4.0.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.2.0", + "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", - "uuid": "^9.0.0", + "uuid": "^10.0.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, @@ -801,6 +803,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/openai": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.11.tgz", + "integrity": "sha512-mEFbpJ8w8NPArsquUlCwxvZTKNkXxqwzvTEYzv6Jb7gUoBDOZtwLg6AdcngTJ+w5VFh3wxgPy0g3zb9Aw0Qbpw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^4.68.0", + "zod": "^3.22.4", + "zod-to-json-schema": "^3.22.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": ">=0.2.26 <0.4.0" + } + }, "node_modules/@microsoft/fetch-event-source": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz", @@ -1221,7 +1254,8 @@ "node_modules/@types/uuid": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz", - "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==" + "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==", + "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", @@ -1783,11 +1817,6 @@ "node": ">=8" } }, - "node_modules/binary-search": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/binary-search/-/binary-search-1.3.6.tgz", - "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==" - }, "node_modules/bplist-parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", @@ -3398,11 +3427,6 @@ "tslib": "^2.4.0" } }, - "node_modules/is-any-array": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-any-array/-/is-any-array-2.0.1.tgz", - "integrity": "sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==" - }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3909,23 +3933,6 @@ } } }, - "node_modules/langchain/node_modules/@langchain/openai": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-0.3.11.tgz", - "integrity": "sha512-mEFbpJ8w8NPArsquUlCwxvZTKNkXxqwzvTEYzv6Jb7gUoBDOZtwLg6AdcngTJ+w5VFh3wxgPy0g3zb9Aw0Qbpw==", - "dependencies": { - "js-tiktoken": "^1.0.12", - "openai": "^4.68.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@langchain/core": ">=0.2.26 <0.4.0" - } - }, "node_modules/langchain/node_modules/@langchain/textsplitters": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@langchain/textsplitters/-/textsplitters-0.1.0.tgz", @@ -3940,15 +3947,23 @@ "@langchain/core": ">=0.2.21 <0.4.0" } }, - "node_modules/langchain/node_modules/@types/uuid": { + "node_modules/langchain/node_modules/uuid": { "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==" + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } }, - "node_modules/langchain/node_modules/langsmith": { + "node_modules/langsmith": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.2.3.tgz", "integrity": "sha512-SPMYPVqR9kwXZVmJ2PXC61HeBnXIFHrjfjDxQ14H0+n5p4gqjLzgSHIQyxBlFeWQUQzArJxe65Ap+s+Xo1cZog==", + "license": "MIT", "dependencies": { "@types/uuid": "^10.0.0", "commander": "^10.0.1", @@ -3966,7 +3981,13 @@ } } }, - "node_modules/langchain/node_modules/uuid": { + "node_modules/langsmith/node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/langsmith/node_modules/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", @@ -3974,25 +3995,11 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, - "node_modules/langsmith": { - "version": "0.0.66", - "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.0.66.tgz", - "integrity": "sha512-yextqrwQiN+2Y0WjHEjQmwS9V6886RIuUG8esibiSh6BTHrtt1WMCAPKJIy8E1+HQvVY7IzsuJ4vzpkKi0wcTQ==", - "dependencies": { - "@types/uuid": "^9.0.1", - "commander": "^10.0.1", - "p-queue": "^6.6.2", - "p-retry": "4", - "uuid": "^9.0.0" - }, - "bin": { - "langsmith": "dist/cli/main.cjs" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4638,46 +4645,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ml-array-mean": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/ml-array-mean/-/ml-array-mean-1.1.6.tgz", - "integrity": "sha512-MIdf7Zc8HznwIisyiJGRH9tRigg3Yf4FldW8DxKxpCCv/g5CafTw0RRu51nojVEOXuCQC7DRVVu5c7XXO/5joQ==", - "dependencies": { - "ml-array-sum": "^1.1.6" - } - }, - "node_modules/ml-array-sum": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/ml-array-sum/-/ml-array-sum-1.1.6.tgz", - "integrity": "sha512-29mAh2GwH7ZmiRnup4UyibQZB9+ZLyMShvt4cH4eTK+cL2oEMIZFnSyB3SS8MlsTh6q/w/yh48KmqLxmovN4Dw==", - "dependencies": { - "is-any-array": "^2.0.0" - } - }, - "node_modules/ml-distance": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ml-distance/-/ml-distance-4.0.1.tgz", - "integrity": "sha512-feZ5ziXs01zhyFUUUeZV5hwc0f5JW0Sh0ckU1koZe/wdVkJdGxcP06KNQuF0WBTj8FttQUzcvQcpcrOp/XrlEw==", - "dependencies": { - "ml-array-mean": "^1.1.6", - "ml-distance-euclidean": "^2.0.0", - "ml-tree-similarity": "^1.0.0" - } - }, - "node_modules/ml-distance-euclidean": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ml-distance-euclidean/-/ml-distance-euclidean-2.0.0.tgz", - "integrity": "sha512-yC9/2o8QF0A3m/0IXqCTXCzz2pNEzvmcE/9HFKOZGnTjatvBbsn4lWYJkxENkA4Ug2fnYl7PXQxnPi21sgMy/Q==" - }, - "node_modules/ml-tree-similarity": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ml-tree-similarity/-/ml-tree-similarity-1.0.0.tgz", - "integrity": "sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg==", - "dependencies": { - "binary-search": "^1.3.5", - "num-sort": "^2.0.0" - } - }, "node_modules/mnth": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mnth/-/mnth-2.0.0.tgz", @@ -4702,6 +4669,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -4822,17 +4798,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/num-sort": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/num-sort/-/num-sort-2.1.0.tgz", - "integrity": "sha512-1MQz1Ed8z2yckoBeSfkQHHO9K1yDRxxtotKSJ9yvcTUUxSvfvzEq5GwBrjjHEpMlq/k5gvXdmJ1SbYxWtpNoVg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6719,6 +6684,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "dev": true, "bin": { "uuid": "dist/bin/uuid" } diff --git a/lib/user-interface/react/package.json b/lib/user-interface/react/package.json index 35fc0026..19759667 100644 --- a/lib/user-interface/react/package.json +++ b/lib/user-interface/react/package.json @@ -16,6 +16,7 @@ "@cloudscape-design/components": "^3.0.638", "@cloudscape-design/global-styles": "^1.0.12", "@langchain/core": "^0.3.5", + "@langchain/openai": "^0.3.11", "@microsoft/fetch-event-source": "^2.0.1", "@reduxjs/toolkit": "^1.9.5", "axios": "^1.7.4", From 4b4956f429e61757978452ae363b306a1a39a883 Mon Sep 17 00:00:00 2001 From: bedanley Date: Fri, 1 Nov 2024 14:27:05 -0600 Subject: [PATCH 23/48] Create LISA Docs Deployment (#166) * Adding LISA documentation deployment stack --- .../workflows/docs.deploy.github-pages.yml | 57 + README.md | 2 + lib/docs/.gitignore | 2 + lib/docs/.vitepress/config.mts | 86 + lib/docs/admin/api-tokens.md | 77 + lib/docs/admin/api.md | 364 +++ lib/docs/admin/architecture.md | 41 + lib/docs/admin/components.md | 55 + lib/docs/admin/deploy.md | 70 + lib/docs/admin/error.md | 69 + lib/docs/admin/getting-started.md | 303 +++ lib/docs/admin/idp.md | 1 + lib/docs/admin/lite-llm.md | 1 + lib/docs/admin/model-management.md | 1 + lib/docs/admin/security.md | 1 + lib/docs/assets/LisaArchitecture.png | Bin 0 -> 232928 bytes lib/docs/assets/LisaChat.png | Bin 0 -> 99793 bytes lib/docs/assets/LisaModelManagement.png | Bin 0 -> 172795 bytes lib/docs/assets/LisaServe.png | Bin 0 -> 139509 bytes lib/docs/config/api-tokens.md | 77 + lib/docs/config/branding.md | 1 + lib/docs/config/features.md | 1 + lib/docs/config/hiding-chat-components.md | 1 + lib/docs/config/model-compatibility.md | 26 + lib/docs/config/model-management-api.md | 1 + lib/docs/config/model-management-ui.md | 1 + lib/docs/config/vector-stores.md | 1 + lib/docs/index.md | 30 + lib/docs/index.ts | 178 ++ lib/docs/package-lock.json | 2255 +++++++++++++++++ lib/docs/package.json | 17 + lib/docs/public/favicon.ico | Bin 0 -> 1150 bytes lib/docs/public/logo.png | Bin 0 -> 2278 bytes lib/docs/user/chat.md | 172 ++ lib/docs/user/context-windows.md | 1 + lib/docs/user/history.md | 1 + lib/docs/user/model-kwargs.md | 1 + lib/docs/user/models.md | 1 + lib/docs/user/nonrag-management.md | 1 + lib/docs/user/prompt-engineering.md | 1 + lib/docs/user/rag.md | 1 + lib/schema.ts | 10 + lib/stages.ts | 96 +- lib/user-interface/react/package-lock.json | 48 +- package-lock.json | 1 + package.json | 6 +- test/cdk/stacks/docs.test.ts | 104 + tsconfig.json | 5 +- 48 files changed, 4098 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/docs.deploy.github-pages.yml create mode 100644 lib/docs/.gitignore create mode 100644 lib/docs/.vitepress/config.mts create mode 100644 lib/docs/admin/api-tokens.md create mode 100644 lib/docs/admin/api.md create mode 100644 lib/docs/admin/architecture.md create mode 100644 lib/docs/admin/components.md create mode 100644 lib/docs/admin/deploy.md create mode 100644 lib/docs/admin/error.md create mode 100644 lib/docs/admin/getting-started.md create mode 100644 lib/docs/admin/idp.md create mode 100644 lib/docs/admin/lite-llm.md create mode 100644 lib/docs/admin/model-management.md create mode 100644 lib/docs/admin/security.md create mode 100644 lib/docs/assets/LisaArchitecture.png create mode 100644 lib/docs/assets/LisaChat.png create mode 100644 lib/docs/assets/LisaModelManagement.png create mode 100644 lib/docs/assets/LisaServe.png create mode 100644 lib/docs/config/api-tokens.md create mode 100644 lib/docs/config/branding.md create mode 100644 lib/docs/config/features.md create mode 100644 lib/docs/config/hiding-chat-components.md create mode 100644 lib/docs/config/model-compatibility.md create mode 100644 lib/docs/config/model-management-api.md create mode 100644 lib/docs/config/model-management-ui.md create mode 100644 lib/docs/config/vector-stores.md create mode 100644 lib/docs/index.md create mode 100644 lib/docs/index.ts create mode 100644 lib/docs/package-lock.json create mode 100644 lib/docs/package.json create mode 100644 lib/docs/public/favicon.ico create mode 100644 lib/docs/public/logo.png create mode 100644 lib/docs/user/chat.md create mode 100644 lib/docs/user/context-windows.md create mode 100644 lib/docs/user/history.md create mode 100644 lib/docs/user/model-kwargs.md create mode 100644 lib/docs/user/models.md create mode 100644 lib/docs/user/nonrag-management.md create mode 100644 lib/docs/user/prompt-engineering.md create mode 100644 lib/docs/user/rag.md create mode 100644 test/cdk/stacks/docs.test.ts diff --git a/.github/workflows/docs.deploy.github-pages.yml b/.github/workflows/docs.deploy.github-pages.yml new file mode 100644 index 00000000..a6162e43 --- /dev/null +++ b/.github/workflows/docs.deploy.github-pages.yml @@ -0,0 +1,57 @@ + +name: Deploy VitePress site to Github Pages +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Install dependencies + working-directory: ./lib/docs + run: npm install + env: + CI: "" + - name: Build with VitePress + working-directory: ./lib/docs + run: npm run docs:build + env: + CI: "" + DOCS_BASE_PATH: '/lisa/' + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./lib/docs/dist + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index e8dec549..2651ce0e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Full Documentation](https://img.shields.io/badge/Full%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/lisa/) + # LLM Inference Solution for Amazon Dedicated Cloud (LISA) ![LISA Architecture](./assets/LisaArchitecture.png) LISA is an infrastructure-as-code solution that supports model hosting and inference. Customers deploy LISA directly diff --git a/lib/docs/.gitignore b/lib/docs/.gitignore new file mode 100644 index 00000000..80ac0588 --- /dev/null +++ b/lib/docs/.gitignore @@ -0,0 +1,2 @@ +dist/ +.vitepress/cache/ diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts new file mode 100644 index 00000000..9b996c9b --- /dev/null +++ b/lib/docs/.vitepress/config.mts @@ -0,0 +1,86 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { defineConfig } from 'vitepress'; + +const navLinks = [ + { + text: 'System Administrator Guide', + items: [ + { text: 'Architecture Overview', link: '/admin/architecture' }, + { text: 'LISA Components', link: '/admin/components' }, + { text: 'Getting Started', link: '/admin/getting-started' }, + { text: 'Configure IdP: Cognito & Keycloak Examples', link: '/admin/idp' }, + { text: 'Deployment', link: '/admin/deploy' }, + { text: 'Setting Model Management Admin Group', link: '/admin/model-management' }, + { text: 'LiteLLM', link: '/admin/lite-llm' }, + { text: 'API Overview', link: '/admin/api' }, + { text: 'API Request Error Handling', link: '/admin/error' }, + { text: 'Security', link: '/admin/security' }, + ], + }, + { + text: 'Advanced Configuration', + items: [ + { text: 'Programmatic API Tokens', link: '/config/api-tokens' }, + { text: 'Model Compatibility', link: '/config/model-compatibility' }, + { text: 'Model Management API', link: '/config/model-management-api' }, + { text: 'Model Management UI', link: '/config/model-management-ui' }, + { text: 'Rag Vector Stores', link: '/config/vector-stores' }, + { text: 'Usage & Features', link: '/config/features' }, + { text: 'Branding', link: '/config/branding' }, + { text: 'Hiding Advanced Chat UI Components', link: '/config/hiding-chat-components' }, + ], + }, + { + text: 'User Guide', + items: [ + { text: 'LISA Chat UI', link: '/user/chat' }, + { text: 'RAG', link: '/user/rag' }, + { text: 'Context Windows', link: '/user/context-windows' }, + { text: 'Model KWARGS', link: '/user/model-kwargs' }, + { text: 'Non-RAG in Context File Management', link: '/user/nonrag-management' }, + { text: 'Prompt Engineering', link: '/user/prompt-engineering' }, + { text: 'Session History', link: '/user/history' }, + ], + }]; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + lang: 'en-US', + title: 'LISA Documentation', + description: 'LLM Inference Solution for Amazon Dedicated Cloud (LISA)', + outDir: 'dist', + base: '/lisa/', + head: [['link', { rel: 'icon', href: '/lisa/favicon.ico' }]], + // https://vitepress.dev/reference/default-theme-config + themeConfig: { + logo: '/logo.png', + nav: [ + { text: 'Home', link: '/' }, + ...navLinks, + ], + + sidebar: navLinks, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/awslabs/lisa' }, + ], + search: { + provider: 'local', + }, + }, +}); diff --git a/lib/docs/admin/api-tokens.md b/lib/docs/admin/api-tokens.md new file mode 100644 index 00000000..1a0afa75 --- /dev/null +++ b/lib/docs/admin/api-tokens.md @@ -0,0 +1,77 @@ +## Programmatic API Tokens + +The LISA Serve ALB can be used for programmatic access outside the example Chat application. +An example use case would be for allowing LISA to serve LLM requests that originate from the [Continue VSCode Plugin](https://www.continue.dev/). +To facilitate communication directly with the LISA Serve ALB, a user with sufficient DynamoDB PutItem permissions may add +API keys to the APITokenTable, and once created, a user may make requests by including the `Authorization: Bearer ${token}` +header or the `Api-Key: ${token}` header with that token. If using any OpenAI-compatible library, the `api_key` fields +will use the `Authorization: Bearer ${token}` format automatically, so there is no need to include additional headers +when using those libraries. + +### Adding a Token + +An account owner may create a long-lived API Token using the following AWS CLI command. + +```bash +AWS_REGION="us-east-1" # change to your deployment region +token_string="YOUR_STRING_HERE" # change to a unique string for a user +aws --region $AWS_REGION dynamodb put-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ + --item '{"token": {"S": "'${token_string}'"}}' +``` + +If an account owner wants the API Token to be temporary and expire after a specific date, LISA will allow for this too. +In addition to the `token` field, the owner may specify the `tokenExpiration` field, which accepts a UNIX timestamp, +in seconds. The following command shows an example of how to do this. + +```bash +AWS_REGION="us-east-1" # change to your deployment region +token_string="YOUR_STRING_HERE" +token_expiration=$(echo $(date +%s) + 3600 | bc) # token that expires in one hour, 3600 seconds +aws --region $AWS_REGION dynamodb put-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ + --item '{ + "token": {"S": "'${token_string}'"}, + "tokenExpiration": {"N": "'${token_expiration}'"} + }' +``` + +Once the token is inserted into the DynamoDB Table, a user may use the token in the `Authorization` request header like +in the following snippet. + +```bash +lisa_serve_rest_url="https://" +token_string="YOUR_STRING_HERE" +curl ${lisa_serve_rest_url}/v2/serve/models \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${token_string}" +``` + +### Updating a Token + +In the case that an owner wishes to change an existing expiration time or add one to a key that did not previously have +an expiration, this can be accomplished by editing the existing item. The following commands can be used as an example +for updating an existing token. Setting the expiration time to a time in the past will effectively remove access for +that key. + +```bash +AWS_REGION="us-east-1" # change to your deployment region +token_string="YOUR_STRING_HERE" +token_expiration=$(echo $(date +%s) + 600 | bc) # token that expires in 10 minutes from now +aws --region $AWS_REGION dynamodb update-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ + --key '{"token": {"S": "'${token_string}'"}}' \ + --update-expression 'SET tokenExpiration=:t' \ + --expression-attribute-values '{":t": {"N": "'${token_expiration}'"}}' +``` + +### Removing a Token + +Tokens will not be automatically removed even if they are no longer valid. An owner may remove an key, expired or not, +from the database to fully revoke the key, by deleting the item. As an example, the following commands can be used to +remove a token. + +```bash +AWS_REGION="us-east-1" # change to your deployment region +token_string="YOUR_STRING_HERE" # change to the token to remove +aws --region $AWS_REGION dynamodb delete-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ + --key '{"token": {"S": "'${token_string}'"}}' +``` diff --git a/lib/docs/admin/api.md b/lib/docs/admin/api.md new file mode 100644 index 00000000..ad9e63e6 --- /dev/null +++ b/lib/docs/admin/api.md @@ -0,0 +1,364 @@ + +# API Usage Overview + +LISA provides robust API endpoints for managing models, both for users and administrators. These endpoints allow for operations such as listing, creating, updating, and deleting models. + +## API Gateway and ALB Endpoints + +LISA uses two primary APIs for model management: + +1. **User-facing OpenAI-Compatible API**: Available to all users for inference tasks and accessible through the LISA Serve ALB. This API provides an interface for querying and interacting with models deployed on Amazon ECS, Amazon Bedrock, or through LiteLLM. +2. **Admin-level Model Management API**: Available only to administrators through the API Gateway (APIGW). This API allows for full control of model lifecycle management, including creating, updating, and deleting models. + +### LiteLLM Routing in All Models + +Every model request is routed through LiteLLM, regardless of whether infrastructure (like ECS) is created for it. Whether deployed on ECS, external models via Bedrock, or managed through LiteLLM, all models are added to LiteLLM for traffic routing. The distinction is whether infrastructure is created (determined by request payloads), but LiteLLM integration is consistent for all models. The model management APIs will handle adding or removing model configurations from LiteLLM, and the LISA Serve endpoint will handle the inference requests against models available in LiteLLM. + +## User-facing OpenAI-Compatible API + +The OpenAI-compatible API is accessible through the LISA Serve ALB and allows users to list models available for inference tasks. Although not specifically part of the model management APIs, any model that is added or removed from LiteLLM via the model management API Gateway APIs will be reflected immediately upon queries to LiteLLM through the LISA Serve ALB. + +### Listing Models + +The `/v2/serve/models` endpoint on the LISA Serve ALB allows users to list all models available for inference in the LISA system. + +#### Request Example: + +```bash +curl -s -H 'Authorization: Bearer ' -X GET https:///v2/serve/models +``` + +#### Response Example: + +```json +{ + "data": [ + { + "id": "bedrock-embed-text-v2", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "titan-express-v1", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "sagemaker-amazon-mistrallite", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + } + ], + "object": "list" +} +``` + +#### Explanation of Response Fields: + +These fields are all defined by the OpenAI API specification, which is documented [here](https://platform.openai.com/docs/api-reference/models/list). + +- `id`: A unique identifier for the model. +- `object`: The type of object, which is "model" in this case. +- `created`: A Unix timestamp representing when the model was created. +- `owned_by`: The entity responsible for the model, such as "openai." + +## Admin-level Model Management API + +This API is only accessible by administrators via the API Gateway and is used to create, update, and delete models. It supports full model lifecycle management. + +### Listing Models (Admin API) + +The `/models` route allows admins to list all models managed by the system. This includes models that are either creating, deleting, already active, or in a failed state. Models can be deployed via ECS or managed externally through a LiteLLM configuration. + +#### Request Example: + +```bash +curl -s -H "Authorization: Bearer " -X GET https:///models +``` + +#### Response Example: + +```json +{ + "models": [ + { + "autoScalingConfig": { + "minCapacity": 1, + "maxCapacity": 1, + "cooldown": 420, + "defaultInstanceWarmup": 180, + "metricConfig": { + "albMetricName": "RequestCountPerTarget", + "targetValue": 30, + "duration": 60, + "estimatedInstanceWarmup": 330 + } + }, + "containerConfig": { + "image": { + "baseImage": "vllm/vllm-openai:v0.5.0", + "type": "asset" + }, + "sharedMemorySize": 2048, + "healthCheckConfig": { + "command": [ + "CMD-SHELL", + "exit 0" + ], + "interval": 10, + "startPeriod": 30, + "timeout": 5, + "retries": 3 + }, + "environment": { + "MAX_TOTAL_TOKENS": "2048", + "MAX_CONCURRENT_REQUESTS": "128", + "MAX_INPUT_LENGTH": "1024" + } + }, + "loadBalancerConfig": { + "healthCheckConfig": { + "path": "/health", + "interval": 60, + "timeout": 30, + "healthyThresholdCount": 2, + "unhealthyThresholdCount": 10 + } + }, + "instanceType": "g5.xlarge", + "modelId": "mistral-vllm", + "modelName": "mistralai/Mistral-7B-Instruct-v0.2", + "modelType": "textgen", + "modelUrl": null, + "status": "Creating", + "streaming": true + }, + { + "autoScalingConfig": null, + "containerConfig": null, + "loadBalancerConfig": null, + "instanceType": null, + "modelId": "titan-express-v1", + "modelName": "bedrock/amazon.titan-text-express-v1", + "modelType": "textgen", + "modelUrl": null, + "status": "InService", + "streaming": true + } + ] +} +``` + +#### Explanation of Response Fields: + +- `modelId`: A unique identifier for the model. +- `modelName`: The name of the model, typically referencing the underlying service (Bedrock, SageMaker, etc.). +- `status`: The current state of the model, e.g., "Creating," "Active," or "Failed." +- `streaming`: Whether the model supports streaming inference. +- `instanceType` (optional): The instance type if the model is deployed via ECS. + +### Creating a Model (Admin API) + +LISA provides the `/models` endpoint for creating both ECS and LiteLLM-hosted models. Depending on the request payload, infrastructure will be created or bypassed (e.g., for LiteLLM-only models). + +This API accepts the same model definition parameters that were accepted in the V2 model definitions within the config.yaml file with one notable difference: the `containerConfig.image.path` field is +now omitted because it corresponded with the `inferenceContainer` selection. As a convenience, this path is no longer required. + +#### Request Example: + +``` +POST https:///models +``` + +#### Example Payload for ECS Model: + +```json +{ + "modelId": "mistral-vllm", + "modelName": "mistralai/Mistral-7B-Instruct-v0.2", + "modelType": "textgen", + "inferenceContainer": "vllm", + "instanceType": "g5.xlarge", + "streaming": true, + "containerConfig": { + "image": { + "baseImage": "vllm/vllm-openai:v0.5.0", + "type": "asset" + }, + "sharedMemorySize": 2048, + "environment": { + "MAX_CONCURRENT_REQUESTS": "128", + "MAX_INPUT_LENGTH": "1024", + "MAX_TOTAL_TOKENS": "2048" + }, + "healthCheckConfig": { + "command": ["CMD-SHELL", "exit 0"], + "interval": 10, + "startPeriod": 30, + "timeout": 5, + "retries": 3 + } + }, + "autoScalingConfig": { + "minCapacity": 1, + "maxCapacity": 1, + "cooldown": 420, + "defaultInstanceWarmup": 180, + "metricConfig": { + "albMetricName": "RequestCountPerTarget", + "targetValue": 30, + "duration": 60, + "estimatedInstanceWarmup": 330 + } + }, + "loadBalancerConfig": { + "healthCheckConfig": { + "path": "/health", + "interval": 60, + "timeout": 30, + "healthyThresholdCount": 2, + "unhealthyThresholdCount": 10 + } + } +} +``` + +#### Creating a LiteLLM-Only Model: + +```json +{ + "modelId": "titan-express-v1", + "modelName": "bedrock/amazon.titan-text-express-v1", + "modelType": "textgen", + "streaming": true +} +``` + +#### Explanation of Key Fields for Creation Payload: + +- `modelId`: The unique identifier for the model. This is any name you would like it to be. +- `modelName`: The name of the model as it appears in the system. For LISA-hosted models, this must be the S3 Key to your model artifacts, otherwise + this is the LiteLLM-compatible reference to a SageMaker Endpoint or Bedrock Foundation Model. Note: Bedrock and SageMaker resources must exist in the + same region as your LISA deployment. If your LISA installation is in us-east-1, then all SageMaker and Bedrock calls will also happen in us-east-1. + Configuration examples: + - LISA hosting: If your model artifacts are in `s3://${lisa_models_bucket}/path/to/model/weights`, then the `modelName` value here should be `path/to/model/weights` + - LiteLLM-only, Bedrock: If you want to use `amazon.titan-text-lite-v1`, your `modelName` value should be `bedrock/amazon.titan-text-lite-v1` + - LiteLLM-only, SageMaker: If you want to use a SageMaker Endpoint named `my-sm-endpoint`, then the `modelName` value should be `sagemaker/my-sm-endpoint`. +- `modelType`: The type of model, such as text generation (textgen). +- `streaming`: Whether the model supports streaming inference. +- `instanceType`: The type of EC2 instance to be used (only applicable for ECS models). +- `containerConfig`: Details about the Docker container, memory allocation, and environment variables. +- `autoScalingConfig`: Configuration related to ECS autoscaling. +- `loadBalancerConfig`: Health check configuration for load balancers. + +### Deleting a Model (Admin API) + +Admins can delete a model using the following endpoint. Deleting a model removes the infrastructure (ECS) or disconnects from LiteLLM. + +#### Request Example: + +``` +DELETE https:///models/{modelId} +``` + +#### Response Example: + +```json +{ + "status": "success", + "message": "Model mistral-vllm has been deleted successfully." +} +``` + +### Updating a Model + +LISA offers basic updating functionality for both LISA-hosted and LiteLLM-only models. For both types, the model type and streaming support can be updated +in the cases that the models were originally created with the wrong parameters. For example, if an embedding model was accidentally created as a `textgen` +model, the UpdateModel API can be used to set it to the intended `embedding` value. Additionally, for LISA-hosted models, users may update the AutoScaling +configuration to increase or decrease capacity usage for each model. Users may use this API to completely shut down all instances behind a model until +they want to add capacity back to the model for usage later. This feature can help users to effectively manage costs so that instances do not have to stay +running in time periods of little or no expected usage. + +The UpdateModel API has mutually exclusive payload fields to avoid conflicting requests. The API does not allow for shutting off a model at the same time +as updating its AutoScaling configuration, as these would introduce ambiguous intents. The API does not allow for setting AutoScaling limits to 0 and instead +requires the usage of the enable/disable functionality to allow models to fully scale down or turn back on. Metadata updates, such as changing the model type +or streaming compatibility, can happen in either type of update or simply by themselves. + +#### Request Example + +``` +PUT https:///models/{modelId} +``` + +#### Example Payloads + +##### Update Model Metadata + +This payload will simply update the model metadata, which will complete within seconds of invoking. If setting a model as an `embedding` model, then the +`streaming` option must be set to `false` or omitted as LISA does not support streaming with embedding models. Both the `streaming` and `modelType` options +may be included in any other update request. + +```json +{ + "streaming": true, + "modelType": "textgen" +} +``` + +##### Update AutoScaling Configuration + +This payload will update the AutoScaling configuration for minimum, maximum, and desired number of instances. The desired number must be between the +minimum or maximum numbers, inclusive, and all the numbers must be strictly greater than 0. If the model currently has less than the minimum number, then +the desired count will automatically raise to the minimum if a desired count is not specified. Despite setting a desired capacity, the model will scale down +to the minimum number over time if you are not hitting the scaling thresholds set when creating the model in the first place. + +The AutoScaling configuration **can** be updated while the model is in the Stopped state, but it won't be applied immediately. Instead, the configuration will +be saved until the model is started again, in which it will use the most recently updated AutoScaling configuration. + +The request will fail if the `autoScalingInstanceConfig` is defined at the same time as the `enabled` field. These options are mutually exclusive and must be +handled as separate operations. Any or all of the options within the `autoScalingInstanceConfig` may be set as needed, so if you only wish to change the `desiredCapacity`, +then that is the only option that you need to specify in the request object within the `autoScalingInstanceConfig`. + +```json +{ + "autoScalingInstanceConfig": { + "minCapacity": 2, + "maxCapacity": 4, + "desiredCapacity": 3 + } +} +``` + +##### Stop Model - Scale Down to 0 Instances + +This payload will stop all model EC2 instances and remove the model reference from LiteLLM so that users are unable to make inference requests against a model +with no capacity. This option is useful for users who wish to manage costs and turn off instances when the model is not currently needed but will be used again +in the future. + +The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be +handled as separate operations. + +```json +{ + "enabled": false +} +``` + +##### Start Model - Restore Previous AutoScaling Configuration + +After stopping a model, this payload will turn the model back on by spinning up instances, waiting for the expected spin-up time to allow models to initialize, and then +adding the reference back to LiteLLM so that users may query the model again. This is expected to be a much faster operation than creating the model through the CreateModel +API, so as long as the model details don't have to change, this in combination with the Stop payload will help to manage costs while still providing model availability as +quickly as the system can spin it up again. + +The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be +handled as separate operations. + +```json +{ + "enabled": true +} +``` diff --git a/lib/docs/admin/architecture.md b/lib/docs/admin/architecture.md new file mode 100644 index 00000000..e0570bea --- /dev/null +++ b/lib/docs/admin/architecture.md @@ -0,0 +1,41 @@ +# LLM Inference Solution for Amazon Dedicated Cloud (LISA) +![LISA Architecture](../assets/LisaArchitecture.png) +LISA is an infrastructure-as-code solution that supports model hosting and inference. Customers deploy LISA directly +into an AWS account and provision their own infrastructure. Customers bring their own models to LISA for hosting and +inference through Amazon ECS. LISA accelerates the use of Generative AI (GenAI) applications by providing scalable, +low latency access to customers’ generative LLMs and embedding language models. Customers can then focus on +experimenting with LLMs and developing GenAI applications. + +LISA’s chatbot user interface can be used for experiment with features and for production use cases. LISA enhances model +output by integrating retrieval-augmented generation (RAG) with Amazon OpenSearch or PostgreSQL’s PGVector extension, +incorporating external knowledge sources into model responses. This helps reduce the need for fine-tuning and delivers +more contextually relevant outputs. + +LISA supports OpenAI’s API Spec via the LiteLLM proxy. This means that LISA is compatible for customers to configure +with models hosted externally by supported model providers. LiteLLM also allows customers to use LISA to standardize +model orchestration and communication across model providers instead of managing each individually. With OpenAI API spec +support, LISA can also be used as a stand-in replacement for any application that already utilizes OpenAI-centric +tooling (ex: OpenAI’s Python library, LangChain). + +## Background + +LISA is a robust, AWS-native platform designed to simplify the deployment and management of Large Language Models (LLMs) in scalable, secure, and highly available environments. Drawing inspiration from the AWS open-source project [aws-genai-llm-chatbot](https://github.com/aws-samples/aws-genai-llm-chatbot), LISA builds on this foundation by offering more specialized functionality, particularly in the areas of security, modularity, and flexibility. + +One of the key differentiators of LISA is its ability to leverage the [text-generation-inference](https://github.com/huggingface/text-generation-inference/tree/main) text-generation-inference container from HuggingFace, allowing users to deploy cutting-edge LLMs. LISA also introduces several innovations that extend beyond its inspiration: + +1. **Support for Amazon Dedicated Cloud (ADC):** LISA is designed to operate in highly controlled environments like Amazon Dedicated Cloud (ADC) partitions, making it ideal for industries with stringent regulatory and security requirements. This focus on secure, isolated deployments differentiates LISA from other open-source platforms. +1. **Modular Design for Composability:** LISA's architecture is designed to be composable, splitting its components into distinct services. The core components, LISA Serve (for LLM serving and inference) and LISA Chat (for the chat interface), can be deployed as independent stacks. This modularity allows users to deploy only the parts they need, enhancing flexibility and scalability across different deployment environments. +1. **OpenAI API Specification Support:** LISA is built to support the OpenAI API specification, allowing users to replace OpenAI’s API with LISA without needing to change existing application code. This makes LISA a drop-in replacement for any workflow or application that already leverages OpenAI’s tooling, such as the OpenAI Python library or LangChain. + +## System Overview + +LISA is designed using a modular, microservices-based architecture, where each service performs a distinct function. It is composed of three core components: LISA Model Management, LISA Serve, and LISA Chat. Each of these components is responsible for specific functionality and interacts via well-defined API endpoints to ensure scalability, security, and fault tolerance across the system. + +**Key System Functionalities:** + +* **Authentication and Authorization** via AWS Cognito or OpenID Connect (OIDC) providers, ensuring secure access to both the REST API and Chat UI through token-based authentication and role-based access control. +* **Model Hosting** on AWS ECS with autoscaling and efficient traffic management using Application Load Balancers (ALBs), providing scalable and high-performance model inference. +* **Model Management** using AWS Step Functions to orchestrate complex workflows for creating, updating, and deleting models, automatically managing underlying ECS infrastructure. +* **Inference Requests** served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. +* **Chat Interface** enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. +* **Retrieval-Augmented Generation (RAG) Operations**, leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. diff --git a/lib/docs/admin/components.md b/lib/docs/admin/components.md new file mode 100644 index 00000000..3df5fda1 --- /dev/null +++ b/lib/docs/admin/components.md @@ -0,0 +1,55 @@ +## LISA Components + +### LISA Model Management +![LISA Model Management Architecture](../assets/LisaModelManagement.png) +The Model Management component is responsible for managing the entire lifecycle of models in LISA. This includes creation, updating, deletion, and scaling of models deployed on ECS. The system automates and scales these operations, ensuring that the underlying infrastructure is managed efficiently. + +* **Model Hosting**: Models are containerized and deployed on AWS ECS, with each model hosted in its own isolated ECS task. This design allows models to be independently scaled based on demand. Traffic to the models is balanced using Application Load Balancers (ALBs), ensuring that the autoscaling mechanism reacts to load fluctuations in real time, optimizing both performance and availability. +* **External Model Routing**: LISA utilizes the LiteLLM proxy to route traffic to different model providers, no matter their API and payload format. Users may add models from external providers, such as SageMaker or Bedrock, to their system to allow requests to models hosted in those systems and services. LISA will simply add the configuration to LiteLLM without creating any additional supporting infrastructure. +* **Model Lifecycle Management**: AWS Step Functions are used to orchestrate the lifecycle of models, handling the creation, update, and deletion workflows. Each workflow provisions the required resources using CloudFormation templates, which manage infrastructure components like EC2 instances, security groups, and ECS services. The system ensures that the necessary security, networking, and infrastructure components are automatically deployed and configured. + * The CloudFormation stacks define essential resources using the LISA core VPC configuration, ensuring best practices for security and access across all resources in the environment. + * DynamoDB stores model metadata, while Amazon S3 securely manages model weights, enabling ECS instances to retrieve the weights dynamically during deployment. + +#### Technical Implementation + +* **Model Lifecycle**: Lifecycle operations such as creation, update, and deletion are executed by Step Functions and backed by AWS Lambda in ```lambda/models/lambda_functions.py```. +* **CloudFormation**: Infrastructure components are provisioned using CloudFormation templates, as defined in ```ecs_model_deployer/src/lib/lisa_model_stack.ts```. +* **ECS Cluster**: ECS cluster and task definitions are located in ```ecs_model_deployer/src/lib/ecsCluster.ts```, with model containers specified in ```ecs_model_deployer/src/lib/ecs-model.ts```. + + +### LISA Serve +![LISA Serve Architecture](../assets/LisaServe.png) +LISA Serve is responsible for processing inference requests and serving model predictions. This component manages user requests to interact with LLMs and ensures that the models deliver low-latency responses. + +* **Inference Requests**: Requests are routed via ALB, which serves as the main entry point to LISA’s backend infrastructure. The ALB forwards requests to the appropriate ECS-hosted model or externally-hosted model based on the request parameters. For models hosted within LISA, traffic to the models is managed with model-specific ALBs, which enable autoscaling if the models are under heavy load. LISA supports both direct REST API-based interaction and interaction through the Chat UI, enabling programmatic access or a user-friendly chat experience. +* **RAG (Retrieval-Augmented Generation)**: RAG operations enhance model responses by integrating external data sources. LISA leverages OpenSearch or PGVector (PostgreSQL) as vector stores, enabling vector-based search and retrieval of relevant knowledge to augment LLM outputs dynamically. + +#### Technical Implementation + +* RAG operations are managed through ```lambda/rag/lambda_functions.py```, which handles embedding generation and document retrieval via OpenSearch and PostgreSQL. +* Direct requests to the LISA Serve ALB entrypoint must utilize the OpenAI API spec, which we support through the use of the LiteLLM proxy. + + +### LISA Chat +![LISA Chatbot Architecture](../assets/LisaChat.png) +LISA Chat provides a customizable chat interface that enables users to interact with models in real-time. This component ensures that users have a seamless experience for submitting queries and maintaining session continuity. + +* **Chat Interface**: The Chat UI is hosted as a static website on Amazon S3 and is served via API Gateway. Users can interact with models directly through the web-based frontend, sending queries and viewing real-time responses from the models. The interface is integrated with LISA's backend services for model inference, retrieval augmented generation, and session management. +* **Session History Management**: LISA maintains session histories using DynamoDB, allowing users to retrieve and continue previous conversations seamlessly. This feature is crucial for maintaining continuity in multi-turn conversations with the models. + +#### Technical Implementation + +* The Chat UI is implemented in the ```lib/user-interface/react/``` folder and is deployed using the scripts in the ```scripts/``` folder. +* Session management logic is handled in ```lambda/session/lambda_functions.py```, where session data is stored and retrieved from DynamoDB. +* RAG operations are defined in lambda/repository/lambda_functions.py + + +## Interaction Flow + +1. **User Interaction with Chat UI or API:** Users can interact with LISA through the Chat UI or REST API. Each interaction is authenticated using AWS Cognito or OIDC, ensuring secure access. +1. **Request Routing:** The API Gateway securely routes user requests to the appropriate backend services, whether for fetching the chat UI, performing RAG operations, or managing models. +1. **Model Management:** Administrators can deploy, update, or delete models via the Model Management API, which triggers ECS deployment and scaling workflows. +1. **Model Inference:** Inference requests are routed to ECS-hosted models or external models via the LiteLLM proxy. Responses are served back to users through the ALB. +1. **RAG Integration:** When RAG is enabled, LISA retrieves relevant documents from OpenSearch or PGVector, augmenting the model's response with external knowledge. +1. **Session Continuity:** User session data is stored in DynamoDB, ensuring that users can retrieve and continue previous conversations across multiple interactions. +1. **Autoscaling:** ECS tasks automatically scale based on system load, with ALBs distributing traffic across available instances to ensure performance. diff --git a/lib/docs/admin/deploy.md b/lib/docs/admin/deploy.md new file mode 100644 index 00000000..bf44b04e --- /dev/null +++ b/lib/docs/admin/deploy.md @@ -0,0 +1,70 @@ + +# Deployment +## Using pre-built resources + +A default configuration will build the necessary containers, lambda layers, and production optimized +web application at build time. In the event that you would like to use pre-built resources due to +network connectivity reasons or other concerns with the environment where you'll be deploying LISA +you can do so. + +- For ECS containers (Models, APIs, etc) you can modify the `containerConfig` block of + the corresponding entry in `config.yaml`. For container images you can provide a path to a directory + from which a docker container will be built (default), a path to a tarball, an ECR repository arn and + optional tag, or a public registry path. + - We provide immediate support for HuggingFace TGI and TEI containers and for vLLM containers. The `example_config.yaml` + file provides examples for TGI and TEI, and the only difference for using vLLM is to change the + `inferenceContainer`, `baseImage`, and `path` options, as indicated in the snippet below. All other options can + remain the same as the model definition examples we have for the TGI or TEI models. vLLM can also support embedding + models in this way, so all you need to do is refer to the embedding model artifacts and remove the `streaming` field + to deploy the embedding model. + - vLLM has support for the OpenAI Embeddings API, but model support for it is limited because the feature is new. Currently, + the only supported embedding model with vLLM is [intfloat/e5-mistral-7b-instruct](https://huggingface.co/intfloat/e5-mistral-7b-instruct), + but this list is expected to grow over time as vLLM updates. + ```yaml + ecsModels: + - modelName: your-model-name + inferenceContainer: tgi + baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 + ``` +- If you are deploying the LISA Chat User Interface you can optionally specify the path to the pre-built + website assets using the top level `webAppAssetsPath` parameter in `config.yaml`. Specifying this path + (typically `lib/user-interface/react/dist`) will avoid using a container to build and bundle the assets + at CDK build time. +- For the lambda layers you can specify the path to a local zip archive of the layer code by including + the optional `lambdaLayerAssets` block in `config.yaml` similar to the following: + +``` +lambdaLayerAssets: + authorizerLayerPath: lib/core/layers/authorizer_layer.zip + commonLayerPath: lib/core/layers/common_layer.zip + fastapiLayerPath: /path/to/fastapi_layer.zip + sdkLayerPath: lib/rag/layers/sdk_layer.zip +``` + +## Deploying + +Now that we have everything setup we are ready to deploy. + +```bash +make deploy +``` + +By default, all stacks will be deployed but a particular stack can be deployed by providing the `STACK` argument to the `deploy` target. + +```bash +make deploy STACK=LisaServe +``` + +Available stacks can be listed by running: + +```bash +make listStacks +``` + +After the `deploy` command is run, you should see many docker build outputs and eventually a CDK progress bar. The deployment should take about 10-15 minutes and will produce a single cloud formation output for the websocket URL. + +You can test the deployment with the integration test: + +```bash +pytest lisa-sdk/tests --url --verify | false +``` diff --git a/lib/docs/admin/error.md b/lib/docs/admin/error.md new file mode 100644 index 00000000..81b9fb78 --- /dev/null +++ b/lib/docs/admin/error.md @@ -0,0 +1,69 @@ + +# Error Handling for API Requests + +In the LISA model management API, error handling is designed to ensure robustness and consistent responses when errors occur during the execution of API requests. This section provides a detailed explanation of the error handling mechanisms in place, including the types of errors that are managed, how they are raised, and what kind of responses clients can expect when these errors occur. + +## Common Errors and Their HTTP Responses + +Below is a list of common errors that can occur in the system, along with the HTTP status codes and response structures that are returned to the client. + +### ModelNotFoundError + +* **Description**: Raised when a model that is requested for retrieval or deletion is not found in the system. +* **HTTP Status Code**: `404 Not Found` +* **Response Body**: + +```json +{ + "error": "ModelNotFoundError", + "message": "The requested model with ID could not be found." +} +``` + +* **Example Scenario**: When a client attempts to fetch details of a model that does not exist in the database, the `ModelNotFoundError` is raised. + +### ModelAlreadyExistsError + +* **Description:** Raised when a request to create a model is made, but the model already exists in the system. +* **HTTP Status Code**: `400` +* **Response Body**: + +```json +{ + "error": "ModelAlreadyExistsError", + "message": "A model with the given configuration already exists." +} +``` + +* **Example Scenario:** A client attempts to create a model with an ID or name that already exists in the database. The system detects the conflict and raises the `ModelAlreadyExistsError`. + +### InvalidInputError (Hypothetical Example) + +* **Description**: Raised when the input provided by the client for creating or updating a model is invalid or does not conform to expected formats. +* **HTTP Status Code**: `400 Bad Request` +* **Response Body**: + +```json +{ + "error": "InvalidInputError", + "message": "The input provided is invalid. Please check the required fields and formats." +} +``` + +* **Example Scenario**: The client submits a malformed JSON body or omits required fields in a model creation request, triggering an `InvalidInputError`. + +## Handling Validation Errors + +Validation errors are handled across the API via utility functions and model transformation logic. These errors typically occur when user inputs fail validation checks or when required data is missing from a request. + +### Example Response for Validation Error: + +* **HTTP Status Code**: `422 Unprocessable Entity` +* **Response Body**: + +```json +{ + "error": "ValidationError", + "message": "The input provided does not meet the required validation criteria." +} +``` diff --git a/lib/docs/admin/getting-started.md b/lib/docs/admin/getting-started.md new file mode 100644 index 00000000..7358be69 --- /dev/null +++ b/lib/docs/admin/getting-started.md @@ -0,0 +1,303 @@ + +# Getting Started with LISA + +LISA (LLM Inference Solution for Amazon Dedicated Cloud) is an advanced infrastructure solution for deploying and +managing Large Language Models (LLMs) on AWS. This guide will walk you through the setup process, from prerequisites +to deployment. + +## Prerequisites + +Before beginning, ensure you have: + +1. An AWS account with appropriate permissions. + 1. Because of all the resource creation that happens as part of CDK deployments, we expect Administrator or Administrator-like permissions with resource creation and mutation permissions. + Installation will not succeed if this profile does not have permissions to create and edit arbitrary resources for the system. + **Note**: This level of permissions is not required for the runtime of LISA, only its deployment and subsequent updates. +2. AWS CLI installed and configured +3. Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles +4. Python 3.9 or later +5. Node.js 14 or later +6. Docker installed and running +7. Sufficient disk space for model downloads and conversions + +If you're new to CDK, review the [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) and consult with your AWS support team. + +> [!TIP] +> To minimize version conflicts and ensure a consistent deployment environment, it is recommended to execute the following steps on a dedicated EC2 instance. However, LISA can be deployed from any machine that meets the prerequisites listed above. + +## Step 1: Clone the Repository + +Ensure you're working with the latest stable release of LISA: + +```bash +git clone -b main --single-branch +cd lisa +``` + +## Step 2: Set Up Environment Variables + +Create and configure your `config.yaml` file: + +```bash +cp example_config.yaml config.yaml +``` + +Set the following environment variables: + +```bash +export PROFILE=my-aws-profile # Optional, can be left blank +export DEPLOYMENT_NAME=my-deployment +export ENV=dev # Options: dev, test, or prod +``` + +## Step 3: Set Up Python and TypeScript Environments + +Install system dependencies and set up both Python and TypeScript environments: + +```bash +# Install system dependencies +sudo apt-get update +sudo apt-get install -y jq + +# Install Python packages +pip3 install --user --upgrade pip +pip3 install yq huggingface_hub s5cmd + +# Set up Python environment +make createPythonEnvironment + +# Activate your python environment +# The command is the output from the previous make command) + +# Install Python Requirements +make installPythonRequirements + +# Set up TypeScript environment +make createTypeScriptEnvironment +make installTypeScriptRequirements +``` + +## Step 4: Configure LISA + +Edit the `config.yaml` file to customize your LISA deployment. Key configurations include: + +- AWS account and region settings +- Model configurations +- Authentication settings +- Networking and infrastructure preferences + +## Step 5: Stage Model Weights + +LISA requires model weights to be staged in the S3 bucket specified in your `config.yaml` file, assuming the S3 bucket follows this structure: + +``` +s3:/// +s3://// +s3://// +... +s3:/// +``` + +**Example:** + +``` +s3:///mistralai/Mistral-7B-Instruct-v0.2 +s3:///mistralai/Mistral-7B-Instruct-v0.2/ +s3:///mistralai/Mistral-7B-Instruct-v0.2/ +... +``` + +To automatically download and stage the model weights defined by the `ecsModels` parameter in your `config.yaml`, use the following command: + +```bash +make modelCheck +``` + +This command verifies if the model's weights are already present in your S3 bucket. If not, it downloads the weights, converts them to the required format, and uploads them to your S3 bucket. Ensure adequate disk space is available for this process. + +> **WARNING** +> As of LISA 3.0, the `ecsModels` parameter in `config.yaml` is solely for staging model weights in your S3 bucket. Previously, before models could be managed through the [API](https://github.com/awslabs/LISA/blob/develop/README.md#creating-a-model-admin-api) or via the Model Management section of the [Chatbot](https://github.com/awslabs/LISA/blob/develop/README.md#chatbot-example), this parameter also dictated which models were deployed. + +> **NOTE** +> For air-gapped systems, before running `make modelCheck` you should manually download model artifacts and place them in a `models` directory at the project root, using the structure: `models/`. + +> **NOTE** +> This process is primarily designed and tested for HuggingFace models. For other model formats, you will need to manually create and upload safetensors. + +## Step 6: Configure Identity Provider + +In the `config.yaml` file, configure the `authConfig` block for authentication. LISA supports OpenID Connect (OIDC) providers such as AWS Cognito or Keycloak. Required fields include: + +- `authority`: URL of your identity provider +- `clientId`: Client ID for your application +- `adminGroup`: Group name for users with model management permissions +- `jwtGroupsProperty`: Path to the groups field in the JWT token +- `additionalScopes` (optional): Extra scopes for group membership information + +#### Cognito Configuration Example: +In Cognito, the `authority` will be the URL to your User Pool. As an example, if your User Pool ID, not the name, is `us-east-1_example`, and if it is +running in `us-east-1`, then the URL to put in the `authority` field would be `https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example`. The `clientId` +can be found in your User Pool's "App integration" tab from within the AWS Management Console, and at the bottom of the page, you will see the list of clients +and their associated Client IDs. The ID here is what we will need for the `clientId` field. + + +```yaml +authConfig: + authority: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example + clientId: your-client-id + adminGroup: AdminGroup + jwtGroupsProperty: cognito:groups +``` + +#### Keycloak Configuration Example: +In Keycloak, the `authority` will be the URL to your Keycloak server. The `clientId` is likely not a random string like in the Cognito clients, and instead +will be a string configured by your Keycloak administrator. Your administrator will be able to give you a client name or create a client for you to use for +this application. Once you have this string, use that as the `clientId` within the `authConfig` block. + +```yaml +authConfig: + authority: https://your-keycloak-server.com + clientId: your-client-name + adminGroup: AdminGroup + jwtGroupsProperty: realm_access.roles +``` + +## Step 7: Configure LiteLLM +We utilize LiteLLM under the hood to allow LISA to respond to the [OpenAI specification](https://platform.openai.com/docs/api-reference). +For LiteLLM configuration, a key must be set up so that the system may communicate with a database for tracking all the models that are added or removed +using the [Model Management API](#admin-level-model-management-api). The key must start with `sk-` and then can be any arbitrary string. We recommend generating a new UUID and then using that as +the key. Configuration example is below. + + +```yaml +litellmConfig: + general_settings: + master_key: sk-00000000-0000-0000-0000-000000000000 # needed for db operations, create your own key # pragma: allowlist-secret + model_list: [] +``` + +**Note**: It is possible to add LiteLLM-only models to this configuration, but it is not recommended as the models in this configuration will not show in the +Chat or Model Management UIs. Instead, use the [Model Management UI](#admin-level-model-management-api) to add or remove LiteLLM-only model configurations. + +## Step 8: Set Up SSL Certificates (Development Only) + +**WARNING: THIS IS FOR DEV ONLY** +When deploying for dev and testing you can use a self-signed certificate for the REST API ALB. You can create this by using the script: `gen-cert.sh` and uploading it to `IAM`. + +```bash +export REGION= +./scripts/gen-certs.sh +aws iam upload-server-certificate --server-certificate-name --certificate-body file://scripts/server.pem --private-key file://scripts/server.key +``` + +Update your `config.yaml` with the certificate ARN: + +```yaml +restApiConfig: + loadBalancerConfig: + sslCertIamArn: arn:aws:iam:::server-certificate/ +``` + +## Step 9: Customize Model Deployment + +In the `ecsModels` section of `config.yaml`, allow our deployment process to pull the model weights for you. + +During the deployment process, LISA will optionally attempt to download your model weights if you specify an optional `ecsModels` +array, this will only work in non ADC regions. Specifically, see the `ecsModels` section of the [example_config.yaml](./example_config.yaml) file. +Here we define the model name, inference container, and baseImage: + +```yaml +ecsModels: + - modelName: your-model-name + inferenceContainer: tgi + baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 +``` + +## Step 10: Bootstrap CDK (If Not Already Done) + +If you haven't bootstrapped your AWS account for CDK: + +```bash +make bootstrap +``` + +## Recommended LiteLLM Configuration Options + +While LISA is designed to be flexible, configuring external models requires careful consideration. The following guide +provides a recommended minimal setup for integrating various model types with LISA using LiteLLM. + +### Configuration Overview + +This example configuration demonstrates how to set up: +1. A SageMaker Endpoint +2. An Amazon Bedrock Model +3. A self-hosted OpenAI-compatible text generation model +4. A self-hosted OpenAI-compatible embedding model + +**Note:** Ensure that all endpoints and models are in the same AWS region as your LISA installation. + +### SageMaker Endpoints and Bedrock Models + +LISA supports adding existing SageMaker Endpoints and Bedrock Models to the LiteLLM configuration. As long as these +services are in the same region as the LISA installation, LISA can use them alongside any other deployed models. + +**To use a SageMaker Endpoint:** +1. Install LISA without initially referencing the SageMaker Endpoint. +2. Create a SageMaker Model using the private subnets of the LISA deployment. +3. This setup allows the LISA REST API container to communicate with any Endpoint using that SageMaker Model. + +**SageMaker Endpoints and Bedrock Models can be configured:** +- Statically at LISA deployment time +- Dynamically using the LISA Model Management API + +**Important:** Endpoints or Models statically defined during LISA deployment cannot be removed or updated using the +LISA Model Management API, and they will not show in the Chat UI. These will only show as part of the OpenAI `/models` API. +Although there is support for it, we recommend using the [Model Management API](#admin-level-model-management-api) instead of the following static configuration. + +### Example Configuration + +```yaml +dev: + litellmConfig: + litellm_settings: + telemetry: false # Disable telemetry to LiteLLM servers (recommended for VPC deployments) + drop_params: true # Ignore unrecognized parameters instead of failing + + model_list: + # 1. SageMaker Endpoint Configuration + - model_name: test-endpoint # Human-readable name, can be anything and will be used for OpenAI API calls + litellm_params: + model: sagemaker/test-endpoint # Prefix required for SageMaker Endpoints and "test-endpoint" matches Endpoint name + api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures + lisa_params: + model_type: textgen + streaming: true + + # 2. Amazon Bedrock Model Configuration + - model_name: bedrock-titan-express # Human-readable name for future OpenAI API calls + litellm_params: + model: bedrock/amazon.titan-text-express-v1 # Prefix required for Bedrock Models, and exact name of Model to use + api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures + lisa_params: + model_type: textgen + streaming: true + + # 3. Custom OpenAI-compatible Text Generation Model + - model_name: custom-openai-model # Used in future OpenAI-compatible calls to LiteLLM + litellm_params: + model: openai/custom-provider/textgen-model # Format: openai// + api_base: https://your-domain-here:443/v1 # Your model's base URI + api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures + lisa_params: + model_type: textgen + streaming: true + + # 4. Custom OpenAI-compatible Embedding Model + - model_name: custom-openai-embedding-model # Used in future OpenAI-compatible calls to LiteLLM + litellm_params: + model: openai/modelProvider/modelName # Prefix required for OpenAI-compatible models followed by model provider and name details + api_base: https://your-domain-here:443/v1 # Your model's base URI + api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures + lisa_params: + model_type: embedding +``` diff --git a/lib/docs/admin/idp.md b/lib/docs/admin/idp.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/admin/idp.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/admin/lite-llm.md b/lib/docs/admin/lite-llm.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/admin/lite-llm.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/admin/model-management.md b/lib/docs/admin/model-management.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/admin/model-management.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/admin/security.md b/lib/docs/admin/security.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/admin/security.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/assets/LisaArchitecture.png b/lib/docs/assets/LisaArchitecture.png new file mode 100644 index 0000000000000000000000000000000000000000..884e275bfc68c3a977f51f491d8ee7e87c4a7ac7 GIT binary patch literal 232928 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

    BPb!EQ?m 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%0A3^dDCK)^_F;lq#XD?ntfDaELLc`@29;v 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 literal 0 HcmV?d00001 diff --git a/lib/docs/assets/LisaModelManagement.png b/lib/docs/assets/LisaModelManagement.png new file mode 100644 index 0000000000000000000000000000000000000000..61d47c4442591f33bf5073826134868c13f383ad GIT binary patch literal 172795 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

    #8g_J4lEzeb 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%0ATmxx3q2Taik6^Dq_N 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, device_map="auto") +``` + +or + +```python +AutoModelForSeq2SeqLM.from_pretrained(, device_map="auto") +``` + +### HuggingFace Embedding Models + +Embedding models often utilize custom codebases and are not as uniform as generation models. For this reason you will likely need to create a new `inferenceContainer`. Follow the [example](https://github.com/awslabs/LISA/tree/develop/lib/serve/ecs-model/embedding/instructor) provided for the `instructor` model. + +### vLLM Models + +In addition to the support we have for the TGI and TEI containers, we support hosting models using the [vLLM container](https://docs.vllm.ai/en/latest/). vLLM abides by the OpenAI specification, and as such allows both text generation and embedding on the models that vLLM supports. +See the [deployment](#deployment) section for details on how to set up the vLLM container for your models. Similar to how the HuggingFace containers will serve safetensor weights downloaded from the +HuggingFace website, vLLM will do the same, and our configuration will allow you to serve these artifacts automatically. vLLM does not have many supported models for embeddings, but as they become available, +LISA will support them as long as the vLLM container version is updated in the config.yaml file and as long as the model's safetensors can be found in S3. diff --git a/lib/docs/config/model-management-api.md b/lib/docs/config/model-management-api.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/config/model-management-api.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/config/model-management-ui.md b/lib/docs/config/model-management-ui.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/config/model-management-ui.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/config/vector-stores.md b/lib/docs/config/vector-stores.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/config/vector-stores.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/index.md b/lib/docs/index.md new file mode 100644 index 00000000..d25cc398 --- /dev/null +++ b/lib/docs/index.md @@ -0,0 +1,30 @@ +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: +name: "LISA Documentation" +text: "LLM Inference Solution for Amazon Dedicated Cloud (LISA)" +actions: +- theme: brand +text: Getting Started +link: /admin/getting-started + +features: +- title: Authentication and Authorization + details: via AWS Cognito or OpenID Connect (OIDC) providers, ensuring secure access to both the REST API and Chat UI through token-based authentication and role-based access control. +- title: Model Hosting + details: on AWS ECS with autoscaling and efficient traffic management using Application Load Balancers (ALBs), providing scalable and high-performance model inference. +- title: Model Management + details: using AWS Step Functions to orchestrate complex workflows for creating, updating, and deleting models, automatically managing underlying ECS infrastructure. +- title: Inference Requests + details: served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. +- title: Chat Interface + details: enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. +- title: Retrieval-Augmented Generation (RAG) Operations + details: leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. + + +### License Notice + +Although this repository is released under the Apache 2.0 license, when configured to use PGVector as a RAG store it uses +the third party `psycopg2-binary` library. The `psycopg2-binary` project's licensing includes the [LGPL with exceptions](https://github.com/psycopg/psycopg2/blob/master/LICENSE) license. diff --git a/lib/docs/index.ts b/lib/docs/index.ts new file mode 100644 index 00000000..74e7aa5f --- /dev/null +++ b/lib/docs/index.ts @@ -0,0 +1,178 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 * as path from 'node:path'; +import * as fs from 'node:fs'; + +import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; +import { AwsIntegration, EndpointType, RestApi } from 'aws-cdk-lib/aws-apigateway'; +import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; +import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; +import { Construct } from 'constructs'; +import { BaseProps } from '../schema'; + +/** + * Properties for DocsStack Construct. + */ +type DocsProps = {} & BaseProps & StackProps; + +/** + * User Interface Construct. + */ +export class LisaDocsStack extends Stack { + + /** + * @param {Construct} scope - The parent or owner of the construct. + * @param {string} id - The unique identifier for the construct within its scope. + * @param {DocsProps} props - The properties of the construct. + */ + constructor (scope: Construct, id: string, props: DocsProps) { + super(scope, id, props); + const { config } = props; + + // Create Docs S3 bucket + const docsBucket = new Bucket(this, 'DocsBucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, + encryption: BucketEncryption.S3_MANAGED, + enforceSSL: true, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + websiteIndexDocument: 'index.html', + websiteErrorDocument: '404.html', + }); + + // Ensure dist folder is created (for tests) + const docsPath = path.join(__dirname, 'dist'); + if (!fs.existsSync(docsPath)) { + fs.mkdirSync(docsPath); + } + // Deploy local folder to S3 + new BucketDeployment(this, 'DeployDocsWebsite', { + sources: [Source.asset(docsPath)], + destinationBucket: docsBucket, + }); + + // REST API GW S3 role + const apiGatewayRole = new Role(this, `${Stack.of(this).stackName}-s3-reader-role`, { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + description: 'Allows API gateway to proxy static website assets', + }); + docsBucket.grantRead(apiGatewayRole); + + // Create API Gateway + const api = new RestApi(this, 'DocsApi', { + description: 'API Gateway for S3 hosted website', + endpointConfiguration: { types: [EndpointType.REGIONAL] }, + deployOptions: { + stageName: 'lisa', + }, + binaryMediaTypes: ['*/*'], + }); + + const defaultIntegration = new AwsIntegration({ + service: 's3', + region: config.region, + integrationHttpMethod: 'GET', + path: `${docsBucket.bucketName}/index.html`, + options: { + credentialsRole: apiGatewayRole, + integrationResponses: [{ + statusCode: '200', + responseParameters: { + 'method.response.header.Content-Type': '\'text/html\'', + }, + }], + }, + }); + + // Create API Gateway integration with S3 + const s3Integration = new AwsIntegration({ + service: 's3', + region: config.region, + integrationHttpMethod: 'GET', + path: `${docsBucket.bucketName}/{key}`, + options: { + credentialsRole: apiGatewayRole, + requestParameters: { + 'integration.request.path.key': 'method.request.path.key', + }, + integrationResponses: [ + { + statusCode: '200', + responseParameters: { + 'method.response.header.Content-Type': 'integration.response.header.Content-Type', + 'method.response.header.Content-Disposition': 'integration.response.header.Content-Disposition', + 'method.response.header.Content-Length': 'integration.response.header.Content-Length', + }, + }, + { + selectionPattern: '403', + statusCode: '404', + responseParameters: { + 'method.response.header.Content-Type': '\'text/html\'', + }, + responseTemplates: { + 'text/html': `#set($context.responseOverride.header.Content-Type = 'text/html') + #set($context.responseOverride.status = 404) + #set($context.responseOverride.header.Location = "$context.domainName/404.html")`, + }, + }, + ], + }, + }); + + // Add GET method to API Gateway + api.root.addMethod('GET', defaultIntegration, { + methodResponses: [ + { + statusCode: '200', + responseParameters: { + 'method.response.header.Content-Type': true, + }, + }, + ], + }); + + api.root.addResource('{key+}').addMethod('GET', s3Integration, { + requestParameters: { + 'method.request.path.key': true, + }, + methodResponses: [ + { + statusCode: '200', + responseParameters: { + 'method.response.header.Content-Type': true, + 'method.response.header.Content-Disposition': true, + 'method.response.header.Content-Length': true, + }, + }, + { + statusCode: '404', + responseParameters: { + 'method.response.header.Content-Type': true, + }, + }, + ], + }); + + // Output the API Gateway URL + new CfnOutput(this, 'DocsApiGatewayUrl', { + value: api.url, + description: 'API Gateway URL', + }); + } +} diff --git a/lib/docs/package-lock.json b/lib/docs/package-lock.json new file mode 100644 index 00000000..587c5e5a --- /dev/null +++ b/lib/docs/package-lock.json @@ -0,0 +1,2255 @@ +{ + "name": "lisa-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lisa-docs", + "version": "1.0.0", + "license": "Apache-2.0", + "devDependencies": { + "vitepress": "^1.4.3" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", + "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", + "@algolia/autocomplete-shared": "1.9.3" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", + "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.6.tgz", + "integrity": "sha512-Cvg5JENdSCMuClwhJ1ON1/jSuojaYMiUW2KePm18IkdCzPJj/NXojaOxw58RFtQFpJgfVW8h2E8mEoDtLlMdeA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.6" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia/node_modules/@algolia/autocomplete-shared": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.6.tgz", + "integrity": "sha512-aq/3V9E00Tw2GC/PqgyPGXtqJUlVc17v4cn1EUhSc+O/4zd04Uwb3UmPm8KDaYQQOrkt1lwvCj2vG2wRE5IKhw==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", + "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.12.0.tgz", + "integrity": "sha512-hx4eVydkm3yrFCFxmcBtSzI/ykt0cZ6sDWch+v3JTgKpD2WtosMJU3Upv1AjQ4B6COSHCOWEX3vfFxW6OoH6aA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.12.0.tgz", + "integrity": "sha512-EpTsSv6IW8maCfXCDIptgT7+mQJj7pImEkcNUnxR8yUKAHzTogTXv9yGm2WXOZFVuwstd2i0sImhQ1Vz8RH/hA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.12.0.tgz", + "integrity": "sha512-od3WmO8qxyfNhKc+K3D17tvun3IMs/xMNmxCG9MiElAkYVbPPTRUYMkRneCpmJyQI0hNx2/EA4kZgzVfQjO86Q==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.12.0.tgz", + "integrity": "sha512-8alajmsYUd+7vfX5lpRNdxqv3Xx9clIHLUItyQK0Z6gwGMbVEFe6YYhgDtwslMAP0y6b0WeJEIZJMLgT7VYpRw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.12.0.tgz", + "integrity": "sha512-bUV9HtfkTBgpoVhxFrMkmVPG03ZN1Rtn51kiaEtukucdk3ggjR9Qu1YUfRSU2lFgxr9qJc8lTxwfvhjCeJRcqw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.12.0.tgz", + "integrity": "sha512-Q5CszzGWfxbIDs9DJ/QJsL7bP6h+lJMg27KxieEnI9KGCu0Jt5iFA3GkREkgRZxRdzlHbZKkrIzhtHVbSHw/rg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.12.0.tgz", + "integrity": "sha512-R3qzEytgVLHOGNri+bpta6NtTt7YtkvUe/QBcAmMDjW4Jk1P0eBYIPfvnzIPbINRsLxIq9fZs9uAYBgsrts4Zg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.12.0.tgz", + "integrity": "sha512-zpHo6qhR22tL8FsdSI4DvEraPDi/019HmMrCFB/TUX98yzh5ooAU7sNW0qPL1I7+S++VbBmNzJOEU9VI8tEC8A==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.12.0.tgz", + "integrity": "sha512-i2AJZED/zf4uhxezAJUhMKoL5QoepCBp2ynOYol0N76+TSoohaMADdPnWCqOULF4RzOwrG8wWynAwBlXsAI1RQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.12.0.tgz", + "integrity": "sha512-0jmZyKvYnB/Bj5c7WKsKedOUjnr0UtXm0LVFUdQrxXfqOqvWv9n6Vpr65UjdYG4Q49kRQxhlwtal9WJYrYymXg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.12.0.tgz", + "integrity": "sha512-KxwleraFuVoEGCoeW6Y1RAEbgBMS7SavqeyzWdtkJc6mXeCOJXn1iZitb8Tyn2FcpMNUKlSm0adrUTt7G47+Ow==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.12.0.tgz", + "integrity": "sha512-FuDZXUGU1pAg2HCnrt8+q1VGHKChV/LhvjvZlLOT7e56GJie6p+EuLu4/hMKPOVuQQ8XXtrTHKIU3Lw+7O5/bQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.12.0.tgz", + "integrity": "sha512-ncDDY7CxZhMs6LIoPl+vHFQceIBhYPY5EfuGF1V7beO0U38xfsCYEyutEFB2kRzf4D9Gqppn3iWX71sNtrKcuw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.3.tgz", + "integrity": "sha512-3uvbg8E7rhqE1C4oBAK3tGlS2qfhi9zpfZgH/yjDPF73vd9B41urVIKujF4rczcF4E3qs34SedhehiDJ4UdNBA==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.6.3.tgz", + "integrity": "sha512-2mBFomaN6VijyQQGwieERDu9GeE0hlv9TQRZBTOYsPQW7/vqtd4hnHEkbBbaBRiS4PYcy+UhikbMuDExJs63UA==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.6.3", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.6.3.tgz", + "integrity": "sha512-2munr4uBuZq1PG+Ge+F+ldIdxb3Wi8OmEIv2tQQb4RvEvvph+xtQkxwHzVIEnt5s+HecwucuXwB+3JhcZboFLg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.9.3", + "@algolia/autocomplete-preset-algolia": "1.17.6", + "@docsearch/css": "3.6.3", + "algoliasearch": "^5.11.0" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz", + "integrity": "sha512-ufb2CH2KfBWPJok95frEZZ82LtDl0A6QKTa8MoM+cWwDZvVGl5/jNb79pIhRvAalUu+7LD91VYR0nwRD799HkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.3.tgz", + "integrity": "sha512-iAHpft/eQk9vkWIV5t22V77d90CRofgR2006UiCjHcHJFVI1E0oBkQIAbz+pLtthFw3hWEmVB4ilxGyBf48i2Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.3.tgz", + "integrity": "sha512-QPW2YmkWLlvqmOa2OwrfqLJqkHm7kJCIMq9kOz40Zo9Ipi40kf9ONG5Sz76zszrmIZZ4hgRIkez69YnTHgEz1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.3.tgz", + "integrity": "sha512-KO0pN5x3+uZm1ZXeIfDqwcvnQ9UEGN8JX5ufhmgH5Lz4ujjZMAnxQygZAVGemFWn+ZZC0FQopruV4lqmGMshow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.24.3.tgz", + "integrity": "sha512-CsC+ZdIiZCZbBI+aRlWpYJMSWvVssPuWqrDy/zi9YfnatKKSLFCe6fjna1grHuo/nVaHG+kiglpRhyBQYRTK4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.24.3.tgz", + "integrity": "sha512-F0nqiLThcfKvRQhZEzMIXOQG4EeX61im61VYL1jo4eBxv4aZRmpin6crnBJQ/nWnCsjH5F6J3W6Stdm0mBNqBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.3.tgz", + "integrity": "sha512-KRSFHyE/RdxQ1CSeOIBVIAxStFC/hnBgVcaiCkQaVC+EYDtTe4X7z5tBkFyRoBgUGtB6Xg6t9t2kulnX6wJc6A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.3.tgz", + "integrity": "sha512-h6Q8MT+e05zP5BxEKz0vi0DhthLdrNEnspdLzkoFqGwnmOzakEHSlXfVyA4HJ322QtFy7biUAVFPvIDEDQa6rw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.3.tgz", + "integrity": "sha512-fKElSyXhXIJ9pqiYRqisfirIo2Z5pTTve5K438URf08fsypXrEkVmShkSfM8GJ1aUyvjakT+fn2W7Czlpd/0FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.3.tgz", + "integrity": "sha512-YlddZSUk8G0px9/+V9PVilVDC6ydMz7WquxozToozSnfFK6wa6ne1ATUjUvjin09jp34p84milxlY5ikueoenw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.3.tgz", + "integrity": "sha512-yNaWw+GAO8JjVx3s3cMeG5Esz1cKVzz8PkTJSfYzE5u7A+NvGmbVFEHP+BikTIyYWuz0+DX9kaA3pH9Sqxp69g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.3.tgz", + "integrity": "sha512-lWKNQfsbpv14ZCtM/HkjCTm4oWTKTfxPmr7iPfp3AHSqyoTz5AgLemYkWLwOBWc+XxBbrU9SCokZP0WlBZM9lA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.3.tgz", + "integrity": "sha512-HoojGXTC2CgCcq0Woc/dn12wQUlkNyfH0I1ABK4Ni9YXyFQa86Fkt2Q0nqgLfbhkyfQ6003i3qQk9pLh/SpAYw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.3.tgz", + "integrity": "sha512-mnEOh4iE4USSccBOtcrjF5nj+5/zm6NcNhbSEfR3Ot0pxBwvEn5QVUXcuOwwPkapDtGZ6pT02xLoPaNv06w7KQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.3.tgz", + "integrity": "sha512-rMTzawBPimBQkG9NKpNHvquIUTQPzrnPxPbCY1Xt+mFkW7pshvyIS5kYgcf74goxXOQk0CP3EoOC1zcEezKXhw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.3.tgz", + "integrity": "sha512-2lg1CE305xNvnH3SyiKwPVsTVLCg4TmNCF1z7PSHX2uZY2VbUpdkgAllVoISD7JO7zu+YynpWNSKAtOrX3AiuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.3.tgz", + "integrity": "sha512-9SjYp1sPyxJsPWuhOCX6F4jUMXGbVVd5obVpoVEi8ClZqo52ViZewA6eFz85y8ezuOA+uJMP5A5zo6Oz4S5rVQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.3.tgz", + "integrity": "sha512-HGZgRFFYrMrP3TJlq58nR1xy8zHKId25vhmm5S9jETEfDf6xybPxsavFTJaufe2zgOGYJBskGlj49CwtEuFhWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.2.tgz", + "integrity": "sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==", + "dev": true, + "dependencies": { + "@shikijs/engine-javascript": "1.22.2", + "@shikijs/engine-oniguruma": "1.22.2", + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.2.tgz", + "integrity": "sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==", + "dev": true, + "dependencies": { + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.2.tgz", + "integrity": "sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==", + "dev": true, + "dependencies": { + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.22.2.tgz", + "integrity": "sha512-8f78OiBa6pZDoZ53lYTmuvpFPlWtevn23bzG+azpPVvZg7ITax57o/K3TC91eYL3OMJOO0onPbgnQyZjRos8XQ==", + "dev": true, + "dependencies": { + "shiki": "1.22.2" + } + }, + "node_modules/@shikijs/types": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.2.tgz", + "integrity": "sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz", + "integrity": "sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.6.2.tgz", + "integrity": "sha512-NCT0ujqlwAhoFvCsAG7G5qS8w/A/dhvFSt2BhmNxyqgpYDrf9CG1zYyWLQkE3dsZ+5lCT6ULUic2VKNaE07Vzg==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.6.2" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.2.tgz", + "integrity": "sha512-k61BxHRmcTtIQZFouF9QWt9nCCNtSdw12lhg8VNtHq5/XOBGD+ewiK27a40UJ8UPYoCJvi80hbvbYr5E/Zeu1g==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.6.2", + "birpc": "^0.2.19", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.2.tgz", + "integrity": "sha512-lcjyJ7hCC0W0kNwnCGMLVTMvDLoZgjcq9BvboPgS+6jQyDul7fpzRSKTGtGhCHoxrDox7qBAKGbAl2Rcf7GE1A==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz", + "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==", + "dev": true, + "dependencies": { + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz", + "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz", + "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.12", + "@vue/runtime-core": "3.5.12", + "@vue/shared": "3.5.12", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz", + "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "vue": "3.5.12" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.2.0.tgz", + "integrity": "sha512-JIUwRcOqOWzcdu1dGlfW04kaJhW3EXnnjJJfLTtddJanymTL7lF1C0+dVVZ/siLfc73mWn+cGP1PE1PKPruRSA==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.2.0", + "@vueuse/shared": "11.2.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.2.0.tgz", + "integrity": "sha512-zGXz3dsxNHKwiD9jPMvR3DAxQEOV6VWIEYTGVSB9PNpk4pTWR+pXrHz9gvXWcP2sTk3W2oqqS6KwWDdntUvNVA==", + "dev": true, + "dependencies": { + "@vueuse/core": "11.2.0", + "@vueuse/shared": "11.2.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.2.0.tgz", + "integrity": "sha512-L0ZmtRmNx+ZW95DmrgD6vn484gSpVeRbgpWevFKXwqqQxW9hnSi2Ppuh2BzMjnbv4aJRiIw8tQatXT9uOB23dQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.2.0.tgz", + "integrity": "sha512-VxFjie0EanOudYSgMErxXfq6fo8vhr5ICI+BuE3I9FnX7ePllEsVrRQ7O6Q1TLgApeLuPKcHQxAXpP+KnlrJsg==", + "dev": true, + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.12.0.tgz", + "integrity": "sha512-psGBRYdGgik8I6m28iAB8xpubvjEt7UQU+w5MAJUA2324WHiGoHap5BPkkjB14rMaXeRts6pmOsrVIglGyOVwg==", + "dev": true, + "dependencies": { + "@algolia/client-abtesting": "5.12.0", + "@algolia/client-analytics": "5.12.0", + "@algolia/client-common": "5.12.0", + "@algolia/client-insights": "5.12.0", + "@algolia/client-personalization": "5.12.0", + "@algolia/client-query-suggestions": "5.12.0", + "@algolia/client-search": "5.12.0", + "@algolia/ingestion": "1.12.0", + "@algolia/monitoring": "1.12.0", + "@algolia/recommend": "5.12.0", + "@algolia/requester-browser-xhr": "5.12.0", + "@algolia/requester-fetch": "5.12.0", + "@algolia/requester-node-http": "5.12.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz", + "integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==", + "dev": true, + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/minisearch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz", + "integrity": "sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==", + "dev": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "dev": true, + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.3.tgz", + "integrity": "sha512-HBW896xR5HGmoksbi3JBDtmVzWiPAYqp7wip50hjQ67JbDz61nyoMPdqu1DvVW9asYb2M65Z20ZHsyJCMqMyDg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.3", + "@rollup/rollup-android-arm64": "4.24.3", + "@rollup/rollup-darwin-arm64": "4.24.3", + "@rollup/rollup-darwin-x64": "4.24.3", + "@rollup/rollup-freebsd-arm64": "4.24.3", + "@rollup/rollup-freebsd-x64": "4.24.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.3", + "@rollup/rollup-linux-arm-musleabihf": "4.24.3", + "@rollup/rollup-linux-arm64-gnu": "4.24.3", + "@rollup/rollup-linux-arm64-musl": "4.24.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.3", + "@rollup/rollup-linux-riscv64-gnu": "4.24.3", + "@rollup/rollup-linux-s390x-gnu": "4.24.3", + "@rollup/rollup-linux-x64-gnu": "4.24.3", + "@rollup/rollup-linux-x64-musl": "4.24.3", + "@rollup/rollup-win32-arm64-msvc": "4.24.3", + "@rollup/rollup-win32-ia32-msvc": "4.24.3", + "@rollup/rollup-win32-x64-msvc": "4.24.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.2.tgz", + "integrity": "sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==", + "dev": true, + "peer": true + }, + "node_modules/shiki": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.2.tgz", + "integrity": "sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==", + "dev": true, + "dependencies": { + "@shikijs/core": "1.22.2", + "@shikijs/engine-javascript": "1.22.2", + "@shikijs/engine-oniguruma": "1.22.2", + "@shikijs/types": "1.22.2", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", + "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.4.3.tgz", + "integrity": "sha512-956c2K2Mr0ubY9bTc2lCJD3g0mgo0mARB1iJC/BqUt4s0AM8Wl60wSU4zbFnzV7X2miFK1XJDKzGZnuEN90umw==", + "dev": true, + "dependencies": { + "@docsearch/css": "^3.6.2", + "@docsearch/js": "^3.6.2", + "@shikijs/core": "^1.22.2", + "@shikijs/transformers": "^1.22.2", + "@shikijs/types": "^1.22.2", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/devtools-api": "^7.5.4", + "@vue/shared": "^3.5.12", + "@vueuse/core": "^11.1.0", + "@vueuse/integrations": "^11.1.0", + "focus-trap": "^7.6.0", + "mark.js": "8.11.1", + "minisearch": "^7.1.0", + "shiki": "^1.22.2", + "vite": "^5.4.10", + "vue": "^3.5.12" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz", + "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-sfc": "3.5.12", + "@vue/runtime-dom": "3.5.12", + "@vue/server-renderer": "3.5.12", + "@vue/shared": "3.5.12" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/lib/docs/package.json b/lib/docs/package.json new file mode 100644 index 00000000..37f32d01 --- /dev/null +++ b/lib/docs/package.json @@ -0,0 +1,17 @@ +{ + "name": "lisa-docs", + "private": true, + "version": "1.0.0", + "description": "Documentation of LISA", + "scripts": { + "build": "vitepress build .", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "vitepress": "^1.4.3" + } +} diff --git a/lib/docs/public/favicon.ico b/lib/docs/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..643489f29eba7ead92767e6540ef75c683c96766 GIT binary patch literal 1150 zcmds%Jx;?w5QQg#9#_;3P8>?#01iOm8-PR;N?Lv*Q>36k6d*+DqxdK(9TbTb#uh(D z%Nx7{YipeYkmzN+Z{Iim(Kti{d_9lw>(g|TXq$+3Ay{x4AI166T9Zq`MG*(*MeZLi z7f|fVxIAzXltG#WR+Qm5axv^7>p`h-QY~Kkr0_hu53&iQgt4oG=ROKb%z5|ieD}DA zmuq46fAW__HNJjR!P$%QkCs)>b8z1OXkO)$hgwe_zZ%`wdgt_+XGxx$)D}OwD)aE} z^N$nW)bQqA4^LKhpN5~ks_^!s{5@ZX>-yt)Rr<$IYB*gT@C;x-fmAoQX3g23`H?xq z+Jn+HjNBJzTJtMC%(X<=&m4W&>wSOh9;Q!>{K6>i?#sH~BT`#L*#=QEaQ;QKOwMqY H8D_r$NYRp- literal 0 HcmV?d00001 diff --git a/lib/docs/public/logo.png b/lib/docs/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a35f38afd376e2dd813515b371ab0c0e389aacf3 GIT binary patch literal 2278 zcmVpr!~!X$wnRJH075{( zDO!AtRi}_Jj-!s^h*+P0=-51%=~QqAh7MB@N_jZIL`rLIV+(Bv;AkH~OUk3$DO&4#$X5Zgff>L=;9X!JuoYOP zlzIjn7Ify*_%NV>XW?%PnN|fkk(mc5;6>z~w%B16xfG2>z&PMFtE zw&b2SGSDXgk3_su4K3j#Et0Jq5U4{0$u`g1^gcwYw)ft;7u_+gOPcETt)`! zuaFT5G4!pGtp0&vKz}r%`}(_}Jprr%HUkGEzOFKQqcDP>1bTt*0y7H*piKofSjG;~ zK2_wvflg~kAME4=jGhHfx?2H>xTOwzuf-evd(!)vze|IMO*&!Vq2^s(Z^SOtw7n|=>< zdrwX`Jg_~0MvHE$bA!qz@P?q?fOFyA4`?3^u%I+7Es< zU-y4;TENJhLd)KEARco?t}gh?j;ej9=}XziWv*k<9~X7TPZJm|OQ z>G}wDNoGkP$ej8&mfRvs|6W5k7#*4L{zxV^wm0E*0_Pa{k{F>20V{R;&W%7Lnb;1- zIw*8D9Sub9F`c3Rbl~hCEZT1aTDUEcTWRTk%E;OnnH2C7;EhN&HYO6N{+6v1tHYF( zy)#nQyJ=r8bR$SzRzxObnR%GG#S^OEaUErNOP5;cY_ian=tU#A@QwFXjg6xE3 z)ItnjqGKK)$IL5{Ez$RFd^!}iD{$G9M#vW&b$+0BPmIub<;x8H2FVrPjbm-{8|&BI_oZdaRtj~iMchPMpZ5uu_4)9mPbg-!42_iDS z3c8l9bUGlXN(PVwco_Wu@;@~LCByh+odvyO)+rq1HEwQX{7BXrOie!LHFd)SIq~Pe z!rA>D>3Lqx`HXf{BNr*|;*noCcmy{$mnAE%Rd2PWcW}3OvSNJ2vdSgh;p^jMSlP^q zSyT19NAAM81;-k%Qe~GUT;(p}2HiY7ba>}LTl$WHwg&sl!?A3fP`=9MQrw8wBps(& zCEYoWTXwE;E2g??+zdCV7I2k&#tcwrIQI~Ixe`}%jF)IF?(*=S>c}5zrL&@^Y*}Uo(^9YrgBsk^q`YW+?~3)xl5_< z%jNt(whVOC&2(~l2?Lz%mM0dU_SNk>GVOJr^*rxhob(RoDl0>0jTf2rbd#smueh6q zS36dF4~O+VPSRbSTKfD8M)pKwiO8Yh>~$oi8@Xi2ziL<|wa<0+?1@WzcW2tt?VfgS zVdd7cd6*jZPf z^_+$5)h7Q@V7=T}l-1)L_m)#zx9yX$4lF;v4hoc9=?{|YOA~0(Ox$Z*_TSE&Q^`Z5m2mj;$~}4h>14`tylNiTlL)(7oT|mavzg?Y zqjkn)_tW39s&v&|;*2fJs!C+us#M|=)vY~G32#i!8Y7v`y16{^zQ*92No0)e#1z7H z6OOi5m6ttOEQ)knQ+dXDwV%;-R=4)vZbo@r&KjkGw)3a(;s1JO|AsIz80NnUD`zLc zu$q4FPqp;+9<7^0ofl0W&Kl zd64K?&j5c%Sa%)E!LfAso}Df1M4Ttg>v>8~$MNY5bUM4`4CE>qynLLSK&NHwC%!8B zZ_fkJUM0KsvoZQexV^69_MFzTbvF*9^3N-O2eEZhK6os#R{#J207*qoM6N<$f*zGx AdH?_b literal 0 HcmV?d00001 diff --git a/lib/docs/user/chat.md b/lib/docs/user/chat.md new file mode 100644 index 00000000..30e82fab --- /dev/null +++ b/lib/docs/user/chat.md @@ -0,0 +1,172 @@ + +# Chatbot Example + +This repository include an example chatbot web application. The react based web application can be optionally deployed to demonstrate the capabilities of LISA Serve. The chatbot consists of a static react based single page application hosted via API GW S3 proxy integration. The app connects to the LISA Serve REST API and an optional RAG API. The app integrates with an OIDC compatible IdP and allows users to interact directly with any of the textgen models hosted with LISA Serve. If the optional RAG stack is deployed then users can also leverage the embeddings models and AWS OpenSearch or PGVector to demonstrate chat with RAG. Chat sessions are maintained in dynamodb table and a number of parameters are exposed through the UI to allow experimentation with various parameters including prompt, temperature, top k, top p, max tokens, and more. + +## Local development + +### Configuring Pre-Commit Hooks + +To ensure code quality and consistency, this project uses pre-commit hooks. These hooks are configured to perform checks, such as linting and formatting, helping to catch potential issues early. These hooks are run automatically on each push to a remote branch but if you wish to run them locally before each commit, follow these steps: + +1. Install pre-commit: `pip install pre-commit` +2. Install the git hook scripts: `pre-commit install` + +The hooks will now run automatically on changed files but if you wish to test them against all files, run the following command: `pre-commit run --all-files`. + +### Run REST API locally + +``` +cd lib/serve/rest-api +pip install -r src/requirements.txt +export AWS_REGION= +export AUTHORITY= +export CLIENT_ID= +export REGISTERED_MODELS_PS_NAME= +export TOKEN_TABLE_NAME="/LISAApiTokenTable" +gunicorn -k uvicorn.workers.UvicornWorker -w 2 -b "0.0.0.0:8080" "src.main:app" +``` + +### Run example chatbot locally + +Create `lib/user-interface/react/public/env.js` file with the following contents: + +``` +window.env = { + AUTHORITY: '', + CLIENT_ID: '', + JWT_GROUPS_PROP: '', + ADMIN_GROUP: '', + CUSTOM_SCOPES:[], + // Alternatively you can set this to be your REST api elb endpoint + RESTAPI_URI: 'http://localhost:8080/', + API_BASE_URL: 'https://${deployment_id}.execute-api.${regional_domain}/${deployment_stage}', + RESTAPI_VERSION: 'v2', + "MODELS": [ + { + "model": "streaming-textgen-model", + "streaming": true, + "modelType": "textgen" + }, + { + "model": "non-streaming-textgen-model", + "streaming": false, + "modelType": "textgen" + }, + { + "model": "embedding-model", + "streaming": null, + "modelType": "embedding" + } + ] +} +``` + +Launch the Chat UI: + +``` +cd lib/user-interface/react/ +npm run dev +``` + +# Usage and Features + +The LISA Serve endpoint can be used independently of the Chat UI, and the following shows a few examples of how to do that. The Serve endpoint +will still validate user auth, so if you have a Bearer token from the IdP configured with LISA, we will honor it, or if you've set up an API +token using the [DynamoDB instructions](#programmatic-api-tokens), we will also accept that. This diagram shows the LISA Serve components that +would be utilized during direct REST API requests. + +## OpenAI Specification Compatibility + +We now provide greater support for the [OpenAI specification](https://platform.openai.com/docs/api-reference) for model inference and embeddings. +We utilize LiteLLM as a proxy for both models we spin up on behalf of the user and additional models configured through the config.yaml file, and because of that, the +LISA REST API endpoint allows for a central location for making text generation and embeddings requests. We support, and are not limited to, the following popular endpoint +routes as long as your underlying models can also respond to them. + +- /models +- /chat/completions +- /completions +- /embeddings + +By supporting the OpenAI spec, we can more easily allow users to integrate their collection of models into their LLM applications and workflows. In LISA, users can authenticate +using their OpenID Connect Identity Provider, or with an API token created through the DynamoDB token workflow as described [here](#programmatic-api-tokens). Once the token +is retrieved, users can use that in direct requests to the LISA Serve REST API. If using the IdP, users must set the 'Authorization' header, otherwise if using the API token, +either the 'Api-Key' header or the 'Authorization' header. After that, requests to `https://${lisa_serve_alb}/v2/serve` will handle the OpenAI API calls. As an example, the following call can list all +models that LISA is aware of, assuming usage of the API token. If you are using a self-signed cert, you must also provide the `--cacert $path` option to specify a CA bundle to trust for SSL verification. + +```shell +curl -s -H 'Api-Key: your-token' -X GET https://${lisa_serve_alb}/v2/serve/models +``` + +If using the IdP, the request would look like the following: + +```shell +curl -s -H 'Authorization: Bearer your-token' -X GET https://${lisa_serve_alb}/v2/serve/models +``` + +When using a library that requests an OpenAI-compatible base_url, you can provide `https://${lisa_serve_alb}/v2/serve` here. All of the OpenAI routes will +automatically be added to the base URL, just as we appended `/models` to the `/v2/serve` route for listing all models tracked by LISA. + + +## Continue JetBrains and VS Code Plugin + +For developers that desire an LLM assistant to help with programming tasks, we support adding LISA as an LLM provider for the [Continue plugin](https://www.continue.dev). +To add LISA as a provider, open up the Continue plugin's `config.json` file and locate the `models` list. In this list, add the following block, replacing the placeholder URL +with your own REST API domain or ALB. The `/v2/serve` is required at the end of the `apiBase`. This configuration requires an API token as created through the [DynamoDB workflow](#programmatic-api-tokens). + +```json +{ + "model": "AUTODETECT", + "title": "LISA", + "apiBase": "https:///v2/serve", + "provider": "openai", + "apiKey": "your-api-token" // pragma: allowlist-secret +} +``` + +Once you save the `config.json` file, the Continue plugin will call the `/models` API to get a list of models at your disposal. The ones provided by LISA will be prefaced +with "LISA" or with the string you place in the `title` field of the config above. Once the configuration is complete and a model is selected, you can use that model to +generate code and perform AI assistant tasks within your development environment. See the [Continue documentation](https://docs.continue.dev/how-to-use-continue) for more +information about its features, capabilities, and usage. + +### Usage in LLM Libraries + +If your workflow includes using libraries, such as [LangChain](https://python.langchain.com/v0.2/docs/introduction/) or [OpenAI](https://github.com/openai/openai-python), +then you can place LISA right in your application by changing only the endpoint and headers for the client objects. As an example, using the OpenAI library, the client would +normally be instantiated and invoked with the following block. + +```python +from openai import OpenAI + +client = OpenAI( + api_key="my_key" # pragma: allowlist-secret not a real key +) +client.models.list() +``` + +To use the models being served by LISA, the client needs only a few changes: + +1. Specify the `base_url` as the LISA Serve ALB, using the /v2/serve route at the end, similar to the apiBase in the [Continue example](#continue-jetbrains-and-vs-code-plugin) +2. Add the API key that you generated from the [token generation steps](#programmatic-api-tokens) as your `api_key` field. +3. If using a self-signed cert, you must provide a certificate path for validating SSL. If you're using an ACM or public cert, then this may be omitted. +1. We provide a convenience function in the `lisa-sdk` for generating a cert path from an IAM certificate ARN if one is provided in the `RESTAPI_SSL_CERT_ARN` environment variable. + +The Code block will now look like this and you can continue to use the library without any other modifications. + +```python +# for self-signed certificates +import boto3 +from lisapy.utils import get_cert_path +# main client library +from openai import DefaultHttpxClient, OpenAI + +iam_client = boto3.client("iam") +cert_path = get_cert_path(iam_client) + +client = OpenAI( + api_key="my_key", # pragma: allowlist-secret not a real key + base_url="https:///v2/serve", + http_client=DefaultHttpxClient(verify=cert_path), # needed for self-signed certs on your ALB, can be omitted otherwise +) +client.models.list() +``` diff --git a/lib/docs/user/context-windows.md b/lib/docs/user/context-windows.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/context-windows.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/user/history.md b/lib/docs/user/history.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/history.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/user/model-kwargs.md b/lib/docs/user/model-kwargs.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/model-kwargs.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/user/models.md b/lib/docs/user/models.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/models.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/user/nonrag-management.md b/lib/docs/user/nonrag-management.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/nonrag-management.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/user/prompt-engineering.md b/lib/docs/user/prompt-engineering.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/prompt-engineering.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/docs/user/rag.md b/lib/docs/user/rag.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/lib/docs/user/rag.md @@ -0,0 +1 @@ +# TODO diff --git a/lib/schema.ts b/lib/schema.ts index 7e780ffe..bd34ccb0 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -679,6 +679,7 @@ const LiteLLMConfig = z.object({ * @property {string[]} [accountNumbersEcr=null] - List of AWS account numbers for ECR repositories. * @property {boolean} [deployRag=false] - Whether to deploy RAG stacks. * @property {boolean} [deployChat=true] - Whether to deploy chat stacks. + * @property {boolean} [deployDocs=true] - Whether to deploy docs stacks. * @property {boolean} [deployUi=true] - Whether to deploy UI stacks. * @property {string} logLevel - Log level for application. * @property {AuthConfigSchema} authConfig - Authorization configuration. @@ -729,6 +730,7 @@ const RawConfigSchema = z .optional(), deployRag: z.boolean().optional().default(true), deployChat: z.boolean().optional().default(true), + deployDocs: z.boolean().optional().default(true), deployUi: z.boolean().optional().default(true), logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]).default('DEBUG'), authConfig: AuthConfigSchema.optional(), @@ -792,6 +794,14 @@ const RawConfigSchema = z message: 'Chat stack is needed for UI stack. You must set deployChat to true if deployUi is true.', }, ) + .refine( + (config) => { + return !(config.deployRag && !config.deployUi); + }, + { + message: 'UI Stack is needed for Rag stack. You must set deployUI to true if deployRag is true.', + }, + ) .refine( (config) => { return ( diff --git a/lib/stages.ts b/lib/stages.ts index 247dc932..19ffce02 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -42,6 +42,7 @@ import { LisaRagStack } from './rag'; import { BaseProps, stackSynthesizerType } from './schema'; import { LisaServeApplicationStack } from './serve'; import { UserInterfaceStack } from './user-interface'; +import { LisaDocsStack } from './docs'; type CustomLisaServeApplicationStageProps = {} & BaseProps; type LisaServeApplicationStageProps = CustomLisaServeApplicationStageProps & StageProps; @@ -171,57 +172,68 @@ export class LisaServeApplicationStage extends Stage { apiDeploymentStack.addDependency(modelsApiDeploymentStack); stacks.push(modelsApiDeploymentStack); - const chatStack = new LisaChatApplicationStack(this, 'LisaChat', { - ...baseStackProps, - authorizer: apiBaseStack.authorizer, - stackName: createCdkId([config.deploymentName, config.appName, 'chat', config.deploymentStage]), - description: `LISA-chat: ${config.deploymentName}-${config.deploymentStage}`, - restApiId: apiBaseStack.restApiId, - rootResourceId: apiBaseStack.rootResourceId, - vpc: networkingStack.vpc, - }); - chatStack.addDependency(apiBaseStack); - chatStack.addDependency(coreStack); - apiDeploymentStack.addDependency(chatStack); - stacks.push(chatStack); - - const uiStack = new UserInterfaceStack(this, 'LisaUserInterface', { - ...baseStackProps, - architecture: ARCHITECTURE, - stackName: createCdkId([config.deploymentName, config.appName, 'ui', config.deploymentStage]), - description: `LISA-user-interface: ${config.deploymentName}-${config.deploymentStage}`, - restApiId: apiBaseStack.restApiId, - rootResourceId: apiBaseStack.rootResourceId, - }); - uiStack.addDependency(chatStack); - uiStack.addDependency(serveStack); - uiStack.addDependency(apiBaseStack); - apiDeploymentStack.addDependency(uiStack); - stacks.push(uiStack); - - if (config.deployRag) { - const ragStack = new LisaRagStack(this, 'LisaRAG', { + if (config.deployChat) { + const chatStack = new LisaChatApplicationStack(this, 'LisaChat', { ...baseStackProps, authorizer: apiBaseStack.authorizer, - description: `LISA-rag: ${config.deploymentName}-${config.deploymentStage}`, - endpointUrl: serveStack.endpointUrl, - modelsPs: serveStack.modelsPs, + stackName: createCdkId([config.deploymentName, config.appName, 'chat', config.deploymentStage]), + description: `LISA-chat: ${config.deploymentName}-${config.deploymentStage}`, restApiId: apiBaseStack.restApiId, rootResourceId: apiBaseStack.rootResourceId, - stackName: createCdkId([config.deploymentName, config.appName, 'rag', config.deploymentStage]), vpc: networkingStack.vpc, }); - ragStack.addDependency(coreStack); - ragStack.addDependency(iamStack); - ragStack.addDependency(apiBaseStack); - stacks.push(ragStack); - - if (config.deployRag) { - uiStack.addDependency(ragStack); - apiDeploymentStack.addDependency(ragStack); + chatStack.addDependency(apiBaseStack); + chatStack.addDependency(coreStack); + apiDeploymentStack.addDependency(chatStack); + stacks.push(chatStack); + + if (config.deployUi) { + const uiStack = new UserInterfaceStack(this, 'LisaUserInterface', { + ...baseStackProps, + architecture: ARCHITECTURE, + stackName: createCdkId([config.deploymentName, config.appName, 'ui', config.deploymentStage]), + description: `LISA-user-interface: ${config.deploymentName}-${config.deploymentStage}`, + restApiId: apiBaseStack.restApiId, + rootResourceId: apiBaseStack.rootResourceId, + }); + uiStack.addDependency(chatStack); + uiStack.addDependency(serveStack); + uiStack.addDependency(apiBaseStack); + apiDeploymentStack.addDependency(uiStack); + stacks.push(uiStack); + + if (config.deployRag) { + const ragStack = new LisaRagStack(this, 'LisaRAG', { + ...baseStackProps, + authorizer: apiBaseStack.authorizer, + description: `LISA-rag: ${config.deploymentName}-${config.deploymentStage}`, + endpointUrl: serveStack.endpointUrl, + modelsPs: serveStack.modelsPs, + restApiId: apiBaseStack.restApiId, + rootResourceId: apiBaseStack.rootResourceId, + stackName: createCdkId([config.deploymentName, config.appName, 'rag', config.deploymentStage]), + vpc: networkingStack.vpc, + }); + ragStack.addDependency(coreStack); + ragStack.addDependency(iamStack); + ragStack.addDependency(apiBaseStack); + stacks.push(ragStack); + + if (config.deployRag) { + uiStack.addDependency(ragStack); + apiDeploymentStack.addDependency(ragStack); + } + } } } + if (config.deployDocs) { + const docsStack = new LisaDocsStack(this, 'LisaDocs', { + ...baseStackProps + }); + stacks.push(docsStack); + } + stacks.push(apiDeploymentStack); // Set resource tags diff --git a/lib/user-interface/react/package-lock.json b/lib/user-interface/react/package-lock.json index 6003ea49..a5d4e3af 100644 --- a/lib/user-interface/react/package-lock.json +++ b/lib/user-interface/react/package-lock.json @@ -741,9 +741,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -2121,9 +2121,9 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-path": { "version": "1.0.9", @@ -3030,9 +3030,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -4689,9 +4689,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -5106,9 +5106,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5130,9 +5130,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -5148,9 +5148,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5972,9 +5972,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } diff --git a/package-lock.json b/package-lock.json index b5881842..0664a897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "lisa", "version": "3.1.0", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "aws-cdk-lib": "2.125.0", diff --git a/package.json b/package.json index a06ca857..41e648f2 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "test": "jest", "cdk": "cdk", "prepare": "husky install", - "migrate-properties": "node ./scripts/migrate-properties.mjs" - }, + "migrate-properties": "node ./scripts/migrate-properties.mjs", + "postinstall": "(cd lib/user-interface/react && npm install) && (cd lib/docs && npm install)", + "postbuild": "(cd lib/user-interface/react && npm build) && (cd lib/docs && npm build)" +}, "devDependencies": { "@aws-cdk/aws-lambda-python-alpha": "2.125.0-alpha.0", "@aws-sdk/client-iam": "^3.490.0", diff --git a/test/cdk/stacks/docs.test.ts b/test/cdk/stacks/docs.test.ts new file mode 100644 index 00000000..e5e2a924 --- /dev/null +++ b/test/cdk/stacks/docs.test.ts @@ -0,0 +1,104 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 * as fs from 'fs'; +import * as path from 'path'; + +import { App, Aspects, Stack, StackProps } from 'aws-cdk-lib'; +import { Annotations, Match } from 'aws-cdk-lib/assertions'; +import { AwsSolutionsChecks, NIST80053R5Checks } from 'cdk-nag'; +import * as yaml from 'js-yaml'; + +import { LisaDocsStack } from '../../../lib/docs/index'; +import { BaseProps, Config, ConfigFile, ConfigSchema } from '../../../lib/schema'; + +const regions = ['us-east-1', 'us-gov-west-1', 'us-gov-east-1', 'us-isob-east-1', 'us-iso-east-1', 'us-iso-west-1']; + +describe.each(regions)('Docs Nag Pack Tests | Region Test: %s', (awsRegion) => { + let app: App; + let stack: Stack; + let config: Config; + let baseStackProps: BaseProps & StackProps; + + beforeAll(() => { + app = new App(); + + // Read configuration file + const configFilePath = path.join(__dirname, '../../../test/cdk/mocks/config.yaml'); + const configFile = yaml.load(fs.readFileSync(configFilePath, 'utf8')) as ConfigFile; + const configEnv = configFile.env || 'dev'; + const configData = configFile[configEnv]; + if (!configData) { + throw new Error(`Configuration for environment "${configEnv}" not found.`); + } + // Validate and parse configuration + try { + config = ConfigSchema.parse(configData); + } catch (error) { + if (error instanceof Error) { + console.error('Error parsing the configuration:', error.message); + } else { + console.error('An unexpected error occurred:', error); + } + process.exit(1); + } + + baseStackProps = { + env: { + account: '012345678901', + region: awsRegion, + }, + config, + }; + }); + + beforeEach(() => { + + stack = new LisaDocsStack(app, 'LisaDocs', { + ...baseStackProps, + }); + + // WHEN + Aspects.of(stack).add(new AwsSolutionsChecks({ verbose: true })); + Aspects.of(stack).add(new NIST80053R5Checks({ verbose: true })); + }); + + afterEach(() => { + app = new App(); + stack = new Stack(); + }); + + //TODO Update expect values to remediate CDK NAG findings and remove debug + test('AwsSolutions CDK NAG Warnings', () => { + const warnings = Annotations.fromStack(stack).findWarning('*', Match.stringLikeRegexp('AwsSolutions-.*')); + expect(warnings.length).toBe(1); + }); + + test('AwsSolutions CDK NAG Errors', () => { + const errors = Annotations.fromStack(stack).findError('*', Match.stringLikeRegexp('AwsSolutions-.*')); + expect(errors.length).toBe(23); + }); + + test('NIST800.53r5 CDK NAG Warnings', () => { + const warnings = Annotations.fromStack(stack).findWarning('*', Match.stringLikeRegexp('NIST.*')); + expect(warnings.length).toBe(0); + }); + + test('NIST800.53r5 CDK NAG Errors', () => { + const errors = Annotations.fromStack(stack).findError('*', Match.stringLikeRegexp('NIST.*')); + expect(errors.length).toBe(13); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 5473529e..747a2f76 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "inlineSources": true, "experimentalDecorators": true, "strictPropertyInitialization": false, - "typeRoots": ["./node_modules/@types"] + "typeRoots": ["./node_modules/@types"], + "outDir": "dist/" }, - "exclude": ["node_modules", "cdk.out", ".git"] + "exclude": ["node_modules", "cdk.out", ".git", "dist"] } From d5037570cd1dbd14d5c050ab17848649a9bb15e7 Mon Sep 17 00:00:00 2001 From: bedanley Date: Fri, 1 Nov 2024 15:03:59 -0600 Subject: [PATCH 24/48] Update docs.deploy.github-pages.yml --- .github/workflows/docs.deploy.github-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.deploy.github-pages.yml b/.github/workflows/docs.deploy.github-pages.yml index a6162e43..2cef1bbf 100644 --- a/.github/workflows/docs.deploy.github-pages.yml +++ b/.github/workflows/docs.deploy.github-pages.yml @@ -36,7 +36,7 @@ jobs: CI: "" - name: Build with VitePress working-directory: ./lib/docs - run: npm run docs:build + run: npm run build env: CI: "" DOCS_BASE_PATH: '/lisa/' From 7f7c7a75ba2e65a4b2b19d61bfeb513745c01c9e Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 1 Nov 2024 15:24:12 -0600 Subject: [PATCH 25/48] Make it so RAG bucket follows config retention policy --- lib/rag/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rag/index.ts b/lib/rag/index.ts index 30460f86..2db54b2d 100644 --- a/lib/rag/index.ts +++ b/lib/rag/index.ts @@ -79,9 +79,9 @@ export class LisaRagStack extends Stack { StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/layerVersion/common`), ); - const bucketName = `${config.deploymentName}-lisaragdocs-${config.accountNumber}`.toLowerCase(); const bucket = new Bucket(this, createCdkId(['LISA', 'RAG', config.deploymentName, config.deploymentStage]), { - bucketName, + removalPolicy: config.removalPolicy, + autoDeleteObjects: config.removalPolicy === RemovalPolicy.DESTROY, cors: [ { allowedMethods: [HttpMethods.GET, HttpMethods.POST], @@ -94,7 +94,7 @@ export class LisaRagStack extends Stack { const baseEnvironment: Record = { REGISTERED_MODELS_PS_NAME: modelsPs.parameterName, - BUCKET_NAME: bucketName, + BUCKET_NAME: bucket.bucketName, CHUNK_SIZE: config.ragFileProcessingConfig!.chunkSize.toString(), CHUNK_OVERLAP: config.ragFileProcessingConfig!.chunkOverlap.toString(), LISA_API_URL_PS_NAME: endpointUrl.parameterName, From c47bde062652ba27100da616762100e740a85a40 Mon Sep 17 00:00:00 2001 From: bedanley Date: Fri, 1 Nov 2024 15:29:34 -0600 Subject: [PATCH 26/48] Update LISA repo name in docs --- .github/workflows/docs.deploy.github-pages.yml | 2 +- README.md | 2 +- lib/docs/.vitepress/config.mts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.deploy.github-pages.yml b/.github/workflows/docs.deploy.github-pages.yml index 2cef1bbf..ab5a0c5b 100644 --- a/.github/workflows/docs.deploy.github-pages.yml +++ b/.github/workflows/docs.deploy.github-pages.yml @@ -39,7 +39,7 @@ jobs: run: npm run build env: CI: "" - DOCS_BASE_PATH: '/lisa/' + DOCS_BASE_PATH: '/LISA/' - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: diff --git a/README.md b/README.md index 2651ce0e..78b4ee0f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Full Documentation](https://img.shields.io/badge/Full%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/lisa/) +[![Full Documentation](https://img.shields.io/badge/Full%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/LISA/) # LLM Inference Solution for Amazon Dedicated Cloud (LISA) ![LISA Architecture](./assets/LisaArchitecture.png) diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts index 9b996c9b..886ca4db 100644 --- a/lib/docs/.vitepress/config.mts +++ b/lib/docs/.vitepress/config.mts @@ -64,8 +64,8 @@ export default defineConfig({ title: 'LISA Documentation', description: 'LLM Inference Solution for Amazon Dedicated Cloud (LISA)', outDir: 'dist', - base: '/lisa/', - head: [['link', { rel: 'icon', href: '/lisa/favicon.ico' }]], + base: '/LISA/', + head: [['link', { rel: 'icon', href: '/LISA/favicon.ico' }]], // https://vitepress.dev/reference/default-theme-config themeConfig: { logo: '/logo.png', @@ -77,7 +77,7 @@ export default defineConfig({ sidebar: navLinks, socialLinks: [ - { icon: 'github', link: 'https://github.com/awslabs/lisa' }, + { icon: 'github', link: 'https://github.com/awslabs/LISA' }, ], search: { provider: 'local', From f832ab534899f5bc4ed7a786c7e3b645ab8e1998 Mon Sep 17 00:00:00 2001 From: bedanley Date: Sat, 2 Nov 2024 00:14:53 -0600 Subject: [PATCH 27/48] Add dark mode logo --- lib/docs/.vitepress/config.mts | 5 ++++- lib/docs/index.md | 39 +++++++++++++++++---------------- lib/docs/package.json | 6 ++--- lib/docs/public/logo-dark.svg | 1 + lib/docs/public/logo-light.svg | 1 + lib/docs/public/logo.png | Bin 2278 -> 0 bytes 6 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 lib/docs/public/logo-dark.svg create mode 100644 lib/docs/public/logo-light.svg delete mode 100644 lib/docs/public/logo.png diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts index 886ca4db..df61b006 100644 --- a/lib/docs/.vitepress/config.mts +++ b/lib/docs/.vitepress/config.mts @@ -68,7 +68,10 @@ export default defineConfig({ head: [['link', { rel: 'icon', href: '/LISA/favicon.ico' }]], // https://vitepress.dev/reference/default-theme-config themeConfig: { - logo: '/logo.png', + logo: { + light: '/logo-light.svg', + dark: '/logo-dark.svg', + }, nav: [ { text: 'Home', link: '/' }, ...navLinks, diff --git a/lib/docs/index.md b/lib/docs/index.md index d25cc398..bccc62ce 100644 --- a/lib/docs/index.md +++ b/lib/docs/index.md @@ -1,28 +1,29 @@ +--- # https://vitepress.dev/reference/default-theme-home-page layout: home hero: -name: "LISA Documentation" -text: "LLM Inference Solution for Amazon Dedicated Cloud (LISA)" -actions: -- theme: brand -text: Getting Started -link: /admin/getting-started + name: "LISA Documentation" + text: "LLM Inference Solution for Amazon Dedicated Cloud (LISA)" + actions: + - theme: brand + text: Getting Started + link: /admin/getting-started features: -- title: Authentication and Authorization - details: via AWS Cognito or OpenID Connect (OIDC) providers, ensuring secure access to both the REST API and Chat UI through token-based authentication and role-based access control. -- title: Model Hosting - details: on AWS ECS with autoscaling and efficient traffic management using Application Load Balancers (ALBs), providing scalable and high-performance model inference. -- title: Model Management - details: using AWS Step Functions to orchestrate complex workflows for creating, updating, and deleting models, automatically managing underlying ECS infrastructure. -- title: Inference Requests - details: served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. -- title: Chat Interface - details: enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. -- title: Retrieval-Augmented Generation (RAG) Operations - details: leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. - + - title: Authentication and Authorization + details: via AWS Cognito or OpenID Connect (OIDC) providers, ensuring secure access to both the REST API and Chat UI through token-based authentication and role-based access control. + - title: Model Hosting + details: on AWS ECS with autoscaling and efficient traffic management using Application Load Balancers (ALBs), providing scalable and high-performance model inference. + - title: Model Management + details: using AWS Step Functions to orchestrate complex workflows for creating, updating, and deleting models, automatically managing underlying ECS infrastructure. + - title: Inference Requests + details: served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. + - title: Chat Interface + details: enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. + - title: Retrieval-Augmented Generation (RAG) Operations + details: leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. +--- ### License Notice diff --git a/lib/docs/package.json b/lib/docs/package.json index 37f32d01..7441aa18 100644 --- a/lib/docs/package.json +++ b/lib/docs/package.json @@ -5,9 +5,9 @@ "description": "Documentation of LISA", "scripts": { "build": "vitepress build .", - "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", - "docs:preview": "vitepress preview docs" + "docs:dev": "vitepress dev .", + "docs:build": "vitepress build .", + "docs:preview": "vitepress preview ." }, "author": "", "license": "Apache-2.0", diff --git a/lib/docs/public/logo-dark.svg b/lib/docs/public/logo-dark.svg new file mode 100644 index 00000000..31321de6 --- /dev/null +++ b/lib/docs/public/logo-dark.svg @@ -0,0 +1 @@ + diff --git a/lib/docs/public/logo-light.svg b/lib/docs/public/logo-light.svg new file mode 100644 index 00000000..af06d08f --- /dev/null +++ b/lib/docs/public/logo-light.svg @@ -0,0 +1 @@ + diff --git a/lib/docs/public/logo.png b/lib/docs/public/logo.png deleted file mode 100644 index a35f38afd376e2dd813515b371ab0c0e389aacf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2278 zcmVpr!~!X$wnRJH075{( zDO!AtRi}_Jj-!s^h*+P0=-51%=~QqAh7MB@N_jZIL`rLIV+(Bv;AkH~OUk3$DO&4#$X5Zgff>L=;9X!JuoYOP zlzIjn7Ify*_%NV>XW?%PnN|fkk(mc5;6>z~w%B16xfG2>z&PMFtE zw&b2SGSDXgk3_su4K3j#Et0Jq5U4{0$u`g1^gcwYw)ft;7u_+gOPcETt)`! zuaFT5G4!pGtp0&vKz}r%`}(_}Jprr%HUkGEzOFKQqcDP>1bTt*0y7H*piKofSjG;~ zK2_wvflg~kAME4=jGhHfx?2H>xTOwzuf-evd(!)vze|IMO*&!Vq2^s(Z^SOtw7n|=>< zdrwX`Jg_~0MvHE$bA!qz@P?q?fOFyA4`?3^u%I+7Es< zU-y4;TENJhLd)KEARco?t}gh?j;ej9=}XziWv*k<9~X7TPZJm|OQ z>G}wDNoGkP$ej8&mfRvs|6W5k7#*4L{zxV^wm0E*0_Pa{k{F>20V{R;&W%7Lnb;1- zIw*8D9Sub9F`c3Rbl~hCEZT1aTDUEcTWRTk%E;OnnH2C7;EhN&HYO6N{+6v1tHYF( zy)#nQyJ=r8bR$SzRzxObnR%GG#S^OEaUErNOP5;cY_ian=tU#A@QwFXjg6xE3 z)ItnjqGKK)$IL5{Ez$RFd^!}iD{$G9M#vW&b$+0BPmIub<;x8H2FVrPjbm-{8|&BI_oZdaRtj~iMchPMpZ5uu_4)9mPbg-!42_iDS z3c8l9bUGlXN(PVwco_Wu@;@~LCByh+odvyO)+rq1HEwQX{7BXrOie!LHFd)SIq~Pe z!rA>D>3Lqx`HXf{BNr*|;*noCcmy{$mnAE%Rd2PWcW}3OvSNJ2vdSgh;p^jMSlP^q zSyT19NAAM81;-k%Qe~GUT;(p}2HiY7ba>}LTl$WHwg&sl!?A3fP`=9MQrw8wBps(& zCEYoWTXwE;E2g??+zdCV7I2k&#tcwrIQI~Ixe`}%jF)IF?(*=S>c}5zrL&@^Y*}Uo(^9YrgBsk^q`YW+?~3)xl5_< z%jNt(whVOC&2(~l2?Lz%mM0dU_SNk>GVOJr^*rxhob(RoDl0>0jTf2rbd#smueh6q zS36dF4~O+VPSRbSTKfD8M)pKwiO8Yh>~$oi8@Xi2ziL<|wa<0+?1@WzcW2tt?VfgS zVdd7cd6*jZPf z^_+$5)h7Q@V7=T}l-1)L_m)#zx9yX$4lF;v4hoc9=?{|YOA~0(Ox$Z*_TSE&Q^`Z5m2mj;$~}4h>14`tylNiTlL)(7oT|mavzg?Y zqjkn)_tW39s&v&|;*2fJs!C+us#M|=)vY~G32#i!8Y7v`y16{^zQ*92No0)e#1z7H z6OOi5m6ttOEQ)knQ+dXDwV%;-R=4)vZbo@r&KjkGw)3a(;s1JO|AsIz80NnUD`zLc zu$q4FPqp;+9<7^0ofl0W&Kl zd64K?&j5c%Sa%)E!LfAso}Df1M4Ttg>v>8~$MNY5bUM4`4CE>qynLLSK&NHwC%!8B zZ_fkJUM0KsvoZQexV^69_MFzTbvF*9^3N-O2eEZhK6os#R{#J207*qoM6N<$f*zGx AdH?_b From 14dc255d529995be9bd5a0e72df3bd69c41105c9 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 5 Nov 2024 12:47:56 -0700 Subject: [PATCH 28/48] in app config management --- example_config.yaml | 4 - lambda/authorizer/lambda_functions.py | 6 +- lambda/configuration/__init__.py | 13 ++ lambda/configuration/lambda_functions.py | 65 +++++++ lib/chat/api/configuration.ts | 173 ++++++++++++++++++ lib/chat/index.ts | 10 + lib/schema.ts | 7 - lib/user-interface/index.ts | 5 - lib/user-interface/react/src/App.tsx | 17 +- .../react/src/components/Topbar.tsx | 10 + .../react/src/components/chatbot/Chat.tsx | 92 +++++----- .../react/src/components/chatbot/Sessions.tsx | 8 +- .../configuration/ActivatedUserComponents.tsx | 79 ++++++++ .../configuration/ConfigurationComponent.tsx | 165 +++++++++++++++++ .../SystemBannerConfiguration.tsx | 123 +++++++++++++ .../create-model/CreateModelModal.tsx | 35 +--- .../system-banner/system-banner.tsx | 8 +- lib/user-interface/react/src/main.tsx | 5 - .../react/src/pages/Configuration.tsx | 28 +++ .../src/shared/modal/confirmation-modal.tsx | 2 +- .../src/shared/model/configuration.model.ts | 77 ++++++++ .../shared/reducers/configuration.reducer.ts | 52 ++++++ .../react/src/shared/reducers/index.ts | 4 +- .../react/src/shared/reducers/user.reducer.ts | 1 + .../react/src/shared/util/utils.ts | 51 ++++++ test/cdk/mocks/config.yaml | 4 - test/cdk/stacks/chat.test.ts | 6 +- 27 files changed, 935 insertions(+), 115 deletions(-) create mode 100644 lambda/configuration/__init__.py create mode 100644 lambda/configuration/lambda_functions.py create mode 100644 lib/chat/api/configuration.ts create mode 100644 lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx create mode 100644 lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx create mode 100644 lib/user-interface/react/src/components/configuration/SystemBannerConfiguration.tsx create mode 100644 lib/user-interface/react/src/pages/Configuration.tsx create mode 100644 lib/user-interface/react/src/shared/model/configuration.model.ts create mode 100644 lib/user-interface/react/src/shared/reducers/configuration.reducer.ts create mode 100644 lib/user-interface/react/src/shared/util/utils.ts diff --git a/example_config.yaml b/example_config.yaml index e481b094..1275ab32 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -21,10 +21,6 @@ dev: # rolePrefix: CustomPrefix # policyPrefix: CustomPrefix # instanceProfilePrefix: CustomPrefix - # systemBanner: - # text: 'LISA System' - # backgroundColor: orange - # fontColor: black # vpcId: vpc-0123456789abcdef, # subnetIds: [subnet-fedcba9876543210, subnet-0987654321fedcba], s3BucketModels: hf-models-gaiic diff --git a/lambda/authorizer/lambda_functions.py b/lambda/authorizer/lambda_functions.py index 1f6ded07..3d535378 100644 --- a/lambda/authorizer/lambda_functions.py +++ b/lambda/authorizer/lambda_functions.py @@ -37,6 +37,7 @@ def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]: # type: i logger.info("REST API authorization handler started") requested_resource = event["resource"] + request_method = event["httpMethod"] id_token = get_id_token(event) @@ -69,7 +70,10 @@ def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]: # type: i username = jwt_data.get("sub", "user") logger.info(f"Deny access to {username} due to non-admin accessing /models api.") return deny_policy - + if requested_resource.startswith("/configuration") and request_method == "PUT" and not is_admin_user: + username = jwt_data.get("sub", "user") + logger.info(f"Deny access to {username} due to non-admin trying to update configuration.") + return deny_policy logger.debug(f"Generated policy: {allow_policy}") logger.info(f"REST API authorization handler completed with 'Allow' for resource {event['methodArn']}") return allow_policy diff --git a/lambda/configuration/__init__.py b/lambda/configuration/__init__.py new file mode 100644 index 00000000..4139ae4d --- /dev/null +++ b/lambda/configuration/__init__.py @@ -0,0 +1,13 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed 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. diff --git a/lambda/configuration/lambda_functions.py b/lambda/configuration/lambda_functions.py new file mode 100644 index 00000000..23ffabdf --- /dev/null +++ b/lambda/configuration/lambda_functions.py @@ -0,0 +1,65 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed 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. + +"""Lambda functions for managing sessions.""" +import json +import logging +import os +import time +from decimal import Decimal +from typing import Any, Dict + +import boto3 +import create_env_variables # noqa: F401 +from botocore.exceptions import ClientError +from utilities.common_functions import api_wrapper, retry_config + +logger = logging.getLogger(__name__) + +dynamodb = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"], config=retry_config) +table = dynamodb.Table(os.environ["CONFIG_TABLE_NAME"]) + + +@api_wrapper +def get_configuration(event: dict, context: dict) -> Dict[str, Any]: + """List configuration entries by configScope from DynamoDB.""" + config_scope = event["queryStringParameters"]["configScope"] + + response = {} + try: + response = table.query( + KeyConditionExpression="#s = :configScope", + ExpressionAttributeNames={"#s": "configScope"}, + ExpressionAttributeValues={":configScope": config_scope}, + ScanIndexForward=False, + ) + except ClientError as error: + if error.response["Error"]["Code"] == "ResourceNotFoundException": + logger.warning(f"No record found with session id: {config_scope}") + else: + logger.exception("Error fetching session") + return response.get("Items", {}) # type: ignore [no-any-return] + + +@api_wrapper +def update_configuration(event: dict, context: dict) -> None: + """Update configuration in DynamoDB.""" + # from https://stackoverflow.com/a/71446846 + body = json.loads(event["body"], parse_float=Decimal) + body["created_at"] = str(Decimal(time.time())) + + try: + table.put_item(Item=body) + except ClientError: + logger.exception("Error updating session in DynamoDB") diff --git a/lib/chat/api/configuration.ts b/lib/chat/api/configuration.ts new file mode 100644 index 00000000..f0a29cef --- /dev/null +++ b/lib/chat/api/configuration.ts @@ -0,0 +1,173 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { IAuthorizer, RestApi } from 'aws-cdk-lib/aws-apigateway'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { Role } from 'aws-cdk-lib/aws-iam'; +import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Construct } from 'constructs'; + +import { PythonLambdaFunction, registerAPIEndpoint } from '../../api-base/utils'; +import { BaseProps } from '../../schema'; +import { createLambdaRole } from '../../core/utils'; +import { Vpc } from '../../networking/vpc'; +import { AwsCustomResource, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; + +/** + * Properties for ConfigurationApi Construct. + * + * @property {IVpc} vpc - Stack VPC + * @property {Layer} commonLayer - Lambda layer for all Lambdas. + * @property {IRestApi} restAPI - REST APIGW for UI and Lambdas + * @property {IRole} lambdaExecutionRole - Execution role for lambdas + * @property {IAuthorizer} authorizer - APIGW authorizer + * @property {ISecurityGroup[]} securityGroups - Security groups for Lambdas + * @property {Map }importedSubnets for application. + */ +type ConfigurationApiProps = { + authorizer: IAuthorizer; + restApiId: string; + rootResourceId: string; + securityGroups?: ISecurityGroup[]; + vpc?: Vpc; +} & BaseProps; + +/** + * API which Maintains config state in DynamoDB + */ +export class ConfigurationApi extends Construct { + constructor (scope: Construct, id: string, props: ConfigurationApiProps) { + super(scope, id); + + const { authorizer, config, restApiId, rootResourceId, securityGroups, vpc } = props; + + // Get common layer based on arn from SSM due to issues with cross stack references + const commonLambdaLayer = LayerVersion.fromLayerVersionArn( + this, + 'configuration-common-lambda-layer', + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/layerVersion/common`), + ); + + // Create DynamoDB table to handle config data + const configTable = new dynamodb.Table(this, 'ConfigurationTable', { + partitionKey: { + name: 'configScope', + type: dynamodb.AttributeType.STRING, + }, + sortKey: { + name: 'versionId', + type: dynamodb.AttributeType.NUMBER, + }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + encryption: dynamodb.TableEncryption.AWS_MANAGED, + removalPolicy: config.removalPolicy, + }); + + const lambdaRole: Role = createLambdaRole(this, config.deploymentName, 'ConfigurationApi', configTable.tableArn); + + // Populate the App Config table with default config + const date = new Date(); + new AwsCustomResource(this, 'lisa-init-ddb-config', { + onCreate: { + service: 'DynamoDB', + action: 'putItem', + physicalResourceId: PhysicalResourceId.of('initConfigData'), + parameters: { + TableName: configTable.tableName, + Item: { + 'versionId': {'N': '0'}, + 'changedBy': {'S': 'System'}, + 'configScope': {'S': 'global'}, + 'changeReason': {'S': 'Initial deployment default config'}, + 'createdAt': {'S': Math.round(date.getTime() / 1000).toString()}, + 'configuration': {'M': { + 'enabledComponents': {'M': { + 'deleteSessionHistory': {'BOOL': 'True'}, + 'viewMetaData': {'BOOL': 'True'}, + 'editKwargs': {'BOOL': 'True'}, + 'editPromptTemplate': {'BOOL': 'True'}, + 'editChatHistoryBuffer': {'BOOL': 'True'}, + 'editNumOfRagDocument': {'BOOL': 'True'}, + 'uploadRagDocs': {'BOOL': 'True'}, + 'uploadContextDocs': {'BOOL': 'True'} + }}, + 'systemBanner': {'M': { + 'isEnabled': {'BOOL': 'False'}, + 'text': {'S': ''}, + 'textColor': {'S': ''}, + 'backgroundColor': {'S': ''} + }} + }} + }, + }, + }, + role: lambdaRole + }); + + const restApi = RestApi.fromRestApiAttributes(this, 'RestApi', { + restApiId: restApiId, + rootResourceId: rootResourceId, + }); + + // Create API Lambda functions + const apis: PythonLambdaFunction[] = [ + { + name: 'get_configuration', + resource: 'configuration', + description: 'Get configuration', + path: 'configuration', + method: 'GET', + environment: { + CONFIG_TABLE_NAME: configTable.tableName + }, + }, + { + name: 'update_configuration', + resource: 'configuration', + description: 'Updates config data', + path: 'configuration/{configScope}', + method: 'PUT', + environment: { + CONFIG_TABLE_NAME: configTable.tableName, + }, + }, + ]; + + apis.forEach((f) => { + const lambdaFunction = registerAPIEndpoint( + this, + restApi, + authorizer, + './lambda', + [commonLambdaLayer], + f, + Runtime.PYTHON_3_10, + lambdaRole, + vpc, + securityGroups, + ); + if (f.method === 'POST' || f.method === 'PUT') { + configTable.grantWriteData(lambdaFunction); + } else if (f.method === 'GET') { + configTable.grantReadData(lambdaFunction); + } else if (f.method === 'DELETE') { + configTable.grantReadWriteData(lambdaFunction); + } + }); + } +} diff --git a/lib/chat/index.ts b/lib/chat/index.ts index db3bbf82..20e2d8e8 100644 --- a/lib/chat/index.ts +++ b/lib/chat/index.ts @@ -23,6 +23,7 @@ import { Construct } from 'constructs'; import { SessionApi } from './api/session'; import { BaseProps } from '../schema'; import { Vpc } from '../networking/vpc'; +import { ConfigurationApi } from './api/configuration'; type CustomLisaChatStackProps = { authorizer: IAuthorizer; @@ -56,5 +57,14 @@ export class LisaChatApplicationStack extends Stack { securityGroups, vpc, }); + + new ConfigurationApi(this, 'ConfigurationApi', { + authorizer, + config, + restApiId, + rootResourceId, + securityGroups, + vpc, + }); } } diff --git a/lib/schema.ts b/lib/schema.ts index bd34ccb0..d644874e 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -765,13 +765,6 @@ const RawConfigSchema = z sdkLayerPath: z.string().optional(), }) .optional(), - systemBanner: z - .object({ - text: z.string(), - backgroundColor: z.string(), - fontColor: z.string(), - }) - .optional(), permissionsBoundaryAspect: z .object({ permissionsBoundaryPolicyName: z.string(), diff --git a/lib/user-interface/index.ts b/lib/user-interface/index.ts index 83b7c9fd..0c4ce8a6 100644 --- a/lib/user-interface/index.ts +++ b/lib/user-interface/index.ts @@ -177,11 +177,6 @@ export class UserInterfaceStack extends Stack { ).stringValue, RESTAPI_VERSION: 'v2', RAG_ENABLED: config.deployRag, - SYSTEM_BANNER: { - text: config.systemBanner?.text, - backgroundColor: config.systemBanner?.backgroundColor, - fontColor: config.systemBanner?.fontColor, - }, API_BASE_URL: config.apiGatewayConfig?.domainName ? '/' : `/${config.deploymentStage}/`, }; diff --git a/lib/user-interface/react/src/App.tsx b/lib/user-interface/react/src/App.tsx index 3259ced2..f0e6e796 100644 --- a/lib/user-interface/react/src/App.tsx +++ b/lib/user-interface/react/src/App.tsx @@ -30,6 +30,8 @@ import { selectCurrentUserIsAdmin } from './shared/reducers/user.reducer'; import ModelManagement from './pages/ModelManagement'; import NotificationBanner from './shared/notification/notification'; import ConfirmationModal, { ConfirmationModalProps } from './shared/modal/confirmation-modal'; +import Configuration from './pages/Configuration'; +import { useGetConfigurationQuery } from './shared/reducers/configuration.reducer'; const PrivateRoute = ({ children }) => { const auth = useAuth(); @@ -58,6 +60,7 @@ function App () { const [showTools, setShowTools] = useState(false); const [tools, setTools] = useState(null); const confirmationModal: ConfirmationModalProps = useAppSelector((state) => state.modal.confirmationModal); + const { data: config } = useGetConfigurationQuery('global', {refetchOnMountOrArgChange: 5}); useEffect(() => { if (tools) { @@ -70,10 +73,10 @@ function App () { const baseHref = document?.querySelector('base')?.getAttribute('href')?.replace(/\/$/, ''); return ( - {window.env.SYSTEM_BANNER?.text && } + {config && config[0]?.configuration.systemBanner.isEnabled && }

    @@ -113,11 +116,19 @@ function App () { } /> + + + + } + /> } /> {confirmationModal && } - {window.env.SYSTEM_BANNER?.text && } + {config && config[0]?.configuration.systemBanner.isEnabled && } ); } diff --git a/lib/user-interface/react/src/components/Topbar.tsx b/lib/user-interface/react/src/components/Topbar.tsx index 2759ca14..efd95a68 100644 --- a/lib/user-interface/react/src/components/Topbar.tsx +++ b/lib/user-interface/react/src/components/Topbar.tsx @@ -68,6 +68,16 @@ function Topbar () { utilities={[ ...(isUserAdmin ? [ + { + type: 'button', + variant: 'link', + text: 'Configuration', + disableUtilityCollapse: false, + external: false, + onClick: () => { + navigate('/configuration'); + }, + }, { type: 'button', variant: 'link', diff --git a/lib/user-interface/react/src/components/chatbot/Chat.tsx b/lib/user-interface/react/src/components/chatbot/Chat.tsx index 2406df8e..548efb24 100644 --- a/lib/user-interface/react/src/components/chatbot/Chat.tsx +++ b/lib/user-interface/react/src/components/chatbot/Chat.tsx @@ -62,8 +62,10 @@ import { ContextUploadModal, RagUploadModal } from './FileUploadModals'; import { ChatOpenAI } from '@langchain/openai'; import { useGetAllModelsQuery } from '../../shared/reducers/model-management.reducer'; import { IModel, ModelStatus, ModelType } from '../../shared/model/model-management.model'; +import { useGetConfigurationQuery } from '../../shared/reducers/configuration.reducer'; export default function Chat ({ sessionId }) { + const { data: config } = useGetConfigurationQuery('global', {refetchOnMountOrArgChange: 5}); const [userPrompt, setUserPrompt] = useState(''); const [humanPrefix, setHumanPrefix] = useState('User'); const [aiPrefix, setAiPrefix] = useState('Assistant'); @@ -595,15 +597,15 @@ export default function Chat ({ sessionId }) { }`} actions={ - + {config && config[0]?.configuration.enabledComponents.uploadContextDocs && - - {window.env.RAG_ENABLED && ( + } + {window.env.RAG_ENABLED && config && config[0]?.configuration.enabledComponents.uploadRagDocs && ( - - - Chat history buffer size: - - - setRagTopK(parseInt(detail.selectedOption.value))} - options={oneThroughTenOptions} - /> - - - + {config && (config[0]?.configuration.enabledComponents.viewMetaData || + config[0]?.configuration.enabledComponents.editKwargs || + config[0]?.configuration.enabledComponents.editPromptTemplate || + config[0]?.configuration.enabledComponents.editChatHistoryBuffer || + config[0]?.configuration.enabledComponents.editNumOfRagDocument) && + + + {config && config[0]?.configuration.enabledComponents.viewMetaData && setShowMetadata(detail.checked)} checked={showMetadata}> + Show metadata + } + {config && config[0]?.configuration.enabledComponents.editKwargs && } + {config && config[0]?.configuration.enabledComponents.editPromptTemplate && } + {config && config[0]?.configuration.enabledComponents.editChatHistoryBuffer && <> + Chat history buffer size: + + + setRagTopK(parseInt(detail.selectedOption.value))} + options={oneThroughTenOptions} + /> + } + + } diff --git a/lib/user-interface/react/src/components/chatbot/Sessions.tsx b/lib/user-interface/react/src/components/chatbot/Sessions.tsx index ca568f43..648a5787 100644 --- a/lib/user-interface/react/src/components/chatbot/Sessions.tsx +++ b/lib/user-interface/react/src/components/chatbot/Sessions.tsx @@ -28,8 +28,10 @@ import { useCollection } from '@cloudscape-design/collection-hooks'; import { v4 as uuidv4 } from 'uuid'; import { LisaChatSession } from '../types'; import { listSessions, deleteSession, deleteUserSessions } from '../utils'; +import { useGetConfigurationQuery } from '../../shared/reducers/configuration.reducer'; export function Sessions () { + const { data: config } = useGetConfigurationQuery('global', {refetchOnMountOrArgChange: 5}); const auth = useAuth(); const [sessions, setSessions] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -115,9 +117,10 @@ export function Sessions () { + {config && config[0]?.configuration.enabledComponents.deleteSessionHistory && + } ), minWidth: 170, @@ -139,6 +142,7 @@ export function Sessions () { > Refresh + {config && config[0].configuration.enabledComponents.deleteSessionHistory && + } } diff --git a/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx b/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx new file mode 100644 index 00000000..eeac84b5 --- /dev/null +++ b/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx @@ -0,0 +1,79 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { + Container, + Grid, + Header, + SpaceBetween, + Toggle, + Box +} from '@cloudscape-design/components'; +import React from 'react'; +import {SetFieldsFunction} from '../../shared/validation'; + +const configurableOperations = { + deleteSessionHistory: 'Delete Session History', + viewMetaData: 'View Chat Meta Data', + editKwargs: 'Edit Kwargs', + editPromptTemplate: 'Update Prompt Template', + editNumOfRagDocument: 'Edit Number of RAG documents', + editChatHistoryBuffer: 'Edit Chat History Buffer', + uploadRagDocs: 'Upload documents to RAG', + uploadContextDocs: 'Upload documents to context', +}; + +export type ActivatedComponentConfigurationProps = { + setFields: SetFieldsFunction; + enabledComponents: {[key: string]: boolean}; +}; + +export function ActivatedUserComponents (props: ActivatedComponentConfigurationProps) { + return ( + + Activated Chat UI Components + + }> + + ({colspan: 3}))}> + {Object.keys(configurableOperations).map((operation) => { + return ( + + + { + const updatedField = {}; + updatedField[`enabledComponents.${operation}`] = detail.checked; + props.setFields(updatedField); + }} + checked={props.enabledComponents[operation]} + data-cy={`Toggle-${operation}`} + > + + +

    {configurableOperations[operation]}

    +
    + ); + })} +
    +
    +
    + ); +} + +export default ActivatedUserComponents; diff --git a/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx b/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx new file mode 100644 index 00000000..b805c149 --- /dev/null +++ b/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx @@ -0,0 +1,165 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 _ from 'lodash'; +import React, { ReactElement, useEffect, useMemo } from 'react'; +import ActivatedUserComponents from './ActivatedUserComponents'; +import SystemBannerConfiguration from './SystemBannerConfiguration'; +import { scrollToInvalid, useValidationReducer } from '../../shared/validation'; +import { IConfiguration, SystemConfiguration, SystemConfigurationSchema } from '../../shared/model/configuration.model'; +import SpaceBetween from '@cloudscape-design/components/space-between'; +import { Button, Header } from '@cloudscape-design/components'; +import { useGetConfigurationQuery, useUpdateConfigurationMutation } from '../../shared/reducers/configuration.reducer'; +import { useAppDispatch, useAppSelector } from '../../config/store'; +import { selectCurrentUsername } from '../../shared/reducers/user.reducer'; +import { getJsonDifference } from '../../shared/util/utils'; +import { setConfirmationModal } from '../../shared/reducers/modal.reducer'; +import { useNotificationService } from '../../shared/util/hooks'; + +export type ConfigState = { + validateAll: boolean; + form: SystemConfiguration; + touched: any; + formSubmitting: boolean; +}; + +export function ConfigurationComponent () : ReactElement { + const dispatch = useAppDispatch(); + const notificationService = useNotificationService(dispatch); + const { data: config, isFetching: isFetchingConfig } = useGetConfigurationQuery('global', {refetchOnMountOrArgChange: true}); + const [ + updateConfigMutation, + { isSuccess: isUpdateSuccess, isError: isUpdateError, error: updateError, isLoading: isUpdating, reset: resetUpdate }, + ] = useUpdateConfigurationMutation(); + const initialForm = SystemConfigurationSchema.parse({}); + const currentUsername = useAppSelector(selectCurrentUsername); + const { state, setState, setFields, touchFields, errors, isValid } = useValidationReducer(SystemConfigurationSchema, { + validateAll: false as boolean, + touched: {}, + formSubmitting: false as boolean, + form: { + ...initialForm + }, + } as ConfigState); + + /** + * Converts a JSON object into an outline structure represented as React nodes. + * + * @param {object} [json={}] - The JSON object to be converted. + * @returns {React.ReactNode[]} - An array of React nodes representing the outline structure. + */ + function jsonToOutline (json = {}) { + const output: React.ReactNode[] = []; + + for (const key in json) { + const value = json[key]; + output.push((
  • {_.startCase(key)}{_.isPlainObject(value) ? '' : `: ${value}`}

  • )); + + if (_.isPlainObject(value)) { + const recursiveJson = jsonToOutline(value); // recursively call + output.push((recursiveJson)); + } + } + return
      {output}
    ; + } + + const changesDiff = useMemo(() => { + return getJsonDifference(config && config[0] ? config[0].configuration : initialForm, state.form); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialForm, state.form]); + + useEffect(() => { + if (!isFetchingConfig && config != null) { + setState({ + ...state, + form: { + ...config[0]?.configuration + } + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config, isFetchingConfig]); + + useEffect(() => { + if (!isUpdating && isUpdateSuccess) { + notificationService.generateNotification('Successfully updated configuration', 'success'); + resetUpdate(); + } else if (!isUpdating && isUpdateError) { + notificationService.generateNotification(`Error updating config: ${updateError.data?.message ?? updateError.data}`, 'error'); + resetUpdate(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isUpdateSuccess, isUpdating, isUpdateError, updateError]); + + function handleSubmit () { + if (isValid && !_.isEmpty(changesDiff)) { + const toSubmit: IConfiguration = { + configuration: state.form, + configScope: 'global', + versionId: Number(config[0]?.versionId) + 1, + changedBy: currentUsername ?? 'Admin', + changeReason: `Changes to: ${Object.keys(changesDiff)}` + }; + dispatch( + setConfirmationModal({ + action: 'Update', + resourceName: 'Configuration', + onConfirm: () => updateConfigMutation(toSubmit), + description: _.isEmpty(changesDiff) ?

    No changes detected

    : jsonToOutline(changesDiff), + })); + } + } + + return ( + +
    + LISA App Configuration +
    + + + + + +
    + ); +} + +export default ConfigurationComponent; diff --git a/lib/user-interface/react/src/components/configuration/SystemBannerConfiguration.tsx b/lib/user-interface/react/src/components/configuration/SystemBannerConfiguration.tsx new file mode 100644 index 00000000..f5507c6a --- /dev/null +++ b/lib/user-interface/react/src/components/configuration/SystemBannerConfiguration.tsx @@ -0,0 +1,123 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { + Box, + Container, + FormField, + Grid, + Header, + Input, + SpaceBetween, + Toggle +} from '@cloudscape-design/components'; +import React from 'react'; +import { SetFieldsFunction, TouchFieldsFunction } from '../../shared/validation'; + +export type SystemBannerConfigurationProps = { + setFields: SetFieldsFunction; + textColor: string; + backgroundColor: string; + text: string; + isEnabled: boolean; + touchFields: TouchFieldsFunction; + errors: any; +}; + +export function SystemBannerConfiguration (props: SystemBannerConfigurationProps) { + return ( + + System Banner + + }> + + + + + { + props.setFields({'systemBanner.isEnabled': detail.checked}); + }} + checked={props.isEnabled!} + > + + +

    Activate System Banner

    +
    + + + + + props.setFields({'systemBanner.textColor': event.target.value}) + } + value={props.textColor} + disabled={!props.isEnabled} + style={{ + border: '2px solid #7F8897', + borderRadius: '6px', + padding: '3px' + }} + /> + +

    Text Color

    +
    +
    + + + + + props.setFields({'systemBanner.backgroundColor': event.target.value}) + } + value={props.backgroundColor} + disabled={!props.isEnabled} + style={{ + border: '2px solid #7F8897', + borderRadius: '6px', + padding: '3px' + }} + /> + +

    Background Color

    +
    +
    +
    + + { + props.setFields({'systemBanner.text': detail.value}); + }} + onBlur={() => props.touchFields(['systemBanner.text'])} + value={props.text} + placeholder='Enter system banner text' + disabled={!props.isEnabled} + /> + +
    +
    + ); +} + +export default SystemBannerConfiguration; diff --git a/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx b/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx index b8127aa7..6080bd99 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/CreateModelModal.tsx @@ -34,6 +34,7 @@ import { ReviewModelChanges } from './ReviewModelChanges'; import { ModifyMethod } from '../../../shared/validation/modify-method'; import { z } from 'zod'; import { SerializedError } from '@reduxjs/toolkit'; +import { getJsonDifference } from '../../../shared/util/utils'; export type CreateModelModalProps = { visible: boolean; @@ -136,40 +137,6 @@ export function CreateModelModal (props: CreateModelModalProps) : ReactElement { resetUpdate(); } - /** - * Computes the difference between two JSON objects, recursively. - * - * This function takes two JSON objects as input and returns a new object that - * contains the differences between the two. Works with nested objects. - * - * @param {object} [obj1={}] - The first JSON object to compare. - * @param {object} [obj2={}] - The second JSON object to compare. - * @returns {object} - A new object containing the differences between the two input objects. - */ - function getJsonDifference (obj1 = {}, obj2 = {}) { - const output = {}, - merged = { ...obj1, ...obj2 }; // has properties of both - - for (const key in merged) { - const value1 = obj1 && Object.keys(obj1).includes(key) ? obj1[key] : undefined; - const value2 = obj2 && Object.keys(obj2).includes(key) ? obj2[key] : undefined; - - if (_.isPlainObject(value1) || _.isPlainObject(value2)) { - const value = getJsonDifference(value1, value2); // recursively call - if (Object.keys(value).length !== 0) { - output[key] = value; - } - - } else { - if (!_.isEqual(value1, value2) && (value1 || value2)) { - output[key] = value2; - // output[key][value2] = value2. - } - } - } - return output; - } - const changesDiff = useMemo(() => { return props.isEdit ? getJsonDifference({ ...props.selectedItems[0], diff --git a/lib/user-interface/react/src/components/system-banner/system-banner.tsx b/lib/user-interface/react/src/components/system-banner/system-banner.tsx index b53aa827..11ad3f56 100644 --- a/lib/user-interface/react/src/components/system-banner/system-banner.tsx +++ b/lib/user-interface/react/src/components/system-banner/system-banner.tsx @@ -16,20 +16,22 @@ import { TextContent } from '@cloudscape-design/components'; import React from 'react'; +import { useGetConfigurationQuery } from '../../shared/reducers/configuration.reducer'; type BannerOptions = { position: 'TOP' | 'BOTTOM'; }; export const SystemBanner = ({ position }: BannerOptions) => { + const { data: config } = useGetConfigurationQuery('global', {refetchOnMountOrArgChange: 5}); const bannerStyle: React.CSSProperties = { width: '100%', position: 'fixed', zIndex: 4999, textAlign: 'center', padding: '2px 0px', - backgroundColor: window.env.SYSTEM_BANNER.backgroundColor, - color: window.env.SYSTEM_BANNER.fontColor, + backgroundColor: config[0]?.configuration.systemBanner.backgroundColor, + color: config[0]?.configuration.systemBanner.textColor, }; if (position === 'TOP') { @@ -41,7 +43,7 @@ export const SystemBanner = ({ position }: BannerOptions) => { return (
    - {window.env.SYSTEM_BANNER.text} + {config[0]?.configuration.systemBanner.text}
    ); diff --git a/lib/user-interface/react/src/main.tsx b/lib/user-interface/react/src/main.tsx index e5cc0c4e..d95a298d 100644 --- a/lib/user-interface/react/src/main.tsx +++ b/lib/user-interface/react/src/main.tsx @@ -36,11 +36,6 @@ declare global { RESTAPI_VERSION: string; RAG_ENABLED: boolean; API_BASE_URL: string; - SYSTEM_BANNER?: { - text: string; - backgroundColor: string; - fontColor: string; - }; }; gitInfo?: { revisionTag?: string; diff --git a/lib/user-interface/react/src/pages/Configuration.tsx b/lib/user-interface/react/src/pages/Configuration.tsx new file mode 100644 index 00000000..91bdfc99 --- /dev/null +++ b/lib/user-interface/react/src/pages/Configuration.tsx @@ -0,0 +1,28 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { ReactElement, useEffect } from 'react'; +import ConfigurationComponent from '../components/configuration/ConfigurationComponent'; + +export function Configuration ({ setTools }): ReactElement { + useEffect(() => { + setTools(null); + }, [setTools]); + + return ; +} + +export default Configuration; diff --git a/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx b/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx index b1102659..af0f16c5 100644 --- a/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx +++ b/lib/user-interface/react/src/shared/modal/confirmation-modal.tsx @@ -27,7 +27,7 @@ export type ConfirmationModalProps = { resourceName: string; onConfirm: () => MutationActionCreatorResult; postConfirm?: CallbackFunction; - description?: string; + description?: string | ReactElement; disabled?: boolean; }; diff --git a/lib/user-interface/react/src/shared/model/configuration.model.ts b/lib/user-interface/react/src/shared/model/configuration.model.ts new file mode 100644 index 00000000..be144edc --- /dev/null +++ b/lib/user-interface/react/src/shared/model/configuration.model.ts @@ -0,0 +1,77 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { z } from 'zod'; + +export type SystemConfiguration = { + systemBanner: ISystemBannerConfiguration, + enabledComponents: IEnabledComponents +}; + +export type IEnabledComponents = { + deleteSessionHistory: boolean; + viewMetaData: boolean; + editKwargs: boolean; + editPromptTemplate: boolean; + editNumOfRagDocument: boolean; + editChatHistoryBuffer: boolean; + uploadRagDocs: boolean; + uploadContextDocs: boolean; +}; + +export type ISystemBannerConfiguration = { + isEnabled: boolean; + text: string; + textColor: string; + backgroundColor: string; +}; + +export type BaseConfiguration = { + configScope: string; + versionId: number; + createdAt?: number; + changedBy: string; + changeReason: string; +}; + +export type IConfiguration = BaseConfiguration & { + configuration: SystemConfiguration; +}; + +export const systemBannerConfigSchema = z.object({ + isEnabled: z.boolean().default(false), + text: z.string().default(''), + textColor: z.string().default(''), + backgroundColor: z.string().default(''), +}).refine((data) => !data.isEnabled || (data.isEnabled && data.text.length >= 1), { + message: 'Text is required when banner is activated.', + path: ['text'] +}); + +export const enabledComponentsSchema = z.object({ + deleteSessionHistory: z.boolean().default(true), + viewMetaData: z.boolean().default(true), + editKwargs: z.boolean().default(true), + editPromptTemplate: z.boolean().default(true), + editChatHistoryBuffer: z.boolean().default(true), + editNumOfRagDocument: z.boolean().default(true), + uploadRagDocs: z.boolean().default(true), + uploadContextDocs: z.boolean().default(true), +}); + +export const SystemConfigurationSchema = z.object({ + systemBanner: systemBannerConfigSchema.default(systemBannerConfigSchema.parse({})), + enabledComponents: enabledComponentsSchema.default(enabledComponentsSchema.parse({})), +}); diff --git a/lib/user-interface/react/src/shared/reducers/configuration.reducer.ts b/lib/user-interface/react/src/shared/reducers/configuration.reducer.ts new file mode 100644 index 00000000..14d17561 --- /dev/null +++ b/lib/user-interface/react/src/shared/reducers/configuration.reducer.ts @@ -0,0 +1,52 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 { createApi } from '@reduxjs/toolkit/query/react'; +import { lisaBaseQuery } from './reducer.utils'; +import { IConfiguration } from '../model/configuration.model'; + +export const configurationApi = createApi({ + reducerPath: 'configuration', + baseQuery: lisaBaseQuery(), + endpoints: (builder) => ({ + getConfiguration: builder.query({ + query: (configScope) => ({ + url: `/configuration?configScope=${configScope}` + }), + providesTags:['configuration'], + }), + updateConfiguration: builder.mutation({ + query: (updatedConfig) => ({ + url: `/configuration/${updatedConfig.configScope}`, + method: 'PUT', + data: updatedConfig + }), + transformErrorResponse: (baseQueryReturnValue) => { + // transform into SerializedError + return { + name: 'Update Configuration Error', + message: baseQueryReturnValue.data?.type === 'RequestValidationError' ? baseQueryReturnValue.data.detail.map((error) => error.msg).join(', ') : baseQueryReturnValue.data.message + }; + }, + invalidatesTags: ['configuration'], + }), + }), +}); + +export const { + useGetConfigurationQuery, + useUpdateConfigurationMutation +} = configurationApi; diff --git a/lib/user-interface/react/src/shared/reducers/index.ts b/lib/user-interface/react/src/shared/reducers/index.ts index 707d2f5e..f2231a4d 100644 --- a/lib/user-interface/react/src/shared/reducers/index.ts +++ b/lib/user-interface/react/src/shared/reducers/index.ts @@ -20,14 +20,16 @@ import userReducer from './user.reducer'; import notificationReducer from './notification.reducer'; import modalReducer from './modal.reducer'; import { modelManagementApi } from './model-management.reducer'; +import { configurationApi } from './configuration.reducer'; const rootReducer: ReducersMapObject = { user: userReducer, notification: notificationReducer, modal: modalReducer, [modelManagementApi.reducerPath]: modelManagementApi.reducer, + [configurationApi.reducerPath]: configurationApi.reducer, }; -export const rootMiddleware = [modelManagementApi.middleware]; +export const rootMiddleware = [modelManagementApi.middleware, configurationApi.middleware]; export default rootReducer; diff --git a/lib/user-interface/react/src/shared/reducers/user.reducer.ts b/lib/user-interface/react/src/shared/reducers/user.reducer.ts index 4c0f499e..1bc69193 100644 --- a/lib/user-interface/react/src/shared/reducers/user.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/user.reducer.ts @@ -33,6 +33,7 @@ export const User = createSlice({ }); export const selectCurrentUserIsAdmin = (state: any) => state.user.info?.isAdmin ?? false; +export const selectCurrentUsername = (state: any) => state.user.info?.preferred_username ?? ''; export const { updateUserState } = User.actions; diff --git a/lib/user-interface/react/src/shared/util/utils.ts b/lib/user-interface/react/src/shared/util/utils.ts new file mode 100644 index 00000000..f5dd7aa5 --- /dev/null +++ b/lib/user-interface/react/src/shared/util/utils.ts @@ -0,0 +1,51 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 _ from 'lodash'; + +/** + * Computes the difference between two JSON objects, recursively. + * + * This function takes two JSON objects as input and returns a new object that + * contains the differences between the two. Works with nested objects. + * + * @param {object} [obj1={}] - The first JSON object to compare. + * @param {object} [obj2={}] - The second JSON object to compare. + * @returns {object} - A new object containing the differences between the two input objects. + */ +export function getJsonDifference (obj1 = {}, obj2 = {}) { + const output = {}, + merged = { ...obj1, ...obj2 }; // has properties of both + + for (const key in merged) { + const value1 = obj1 && Object.keys(obj1).includes(key) ? obj1[key] : undefined; + const value2 = obj2 && Object.keys(obj2).includes(key) ? obj2[key] : undefined; + + if (_.isPlainObject(value1) || _.isPlainObject(value2)) { + const value = getJsonDifference(value1, value2); // recursively call + if (Object.keys(value).length !== 0) { + output[key] = value; + } + + } else { + if (!_.isEqual(value1, value2) && (value1 || value2)) { + output[key] = value2; + // output[key][value2] = value2. + } + } + } + return output; +} diff --git a/test/cdk/mocks/config.yaml b/test/cdk/mocks/config.yaml index 059f97f7..e0991c48 100644 --- a/test/cdk/mocks/config.yaml +++ b/test/cdk/mocks/config.yaml @@ -21,10 +21,6 @@ dev: # rolePrefix: CustomPrefix # policyPrefix: CustomPrefix # instanceProfilePrefix: CustomPrefix - # systemBanner: - # text: 'LISA System' - # backgroundColor: orange - # fontColor: black s3BucketModels: hf-models-gaiic # aws partition mountS3 package location mountS3DebUrl: https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.deb diff --git a/test/cdk/stacks/chat.test.ts b/test/cdk/stacks/chat.test.ts index ac766ff2..304adcf0 100644 --- a/test/cdk/stacks/chat.test.ts +++ b/test/cdk/stacks/chat.test.ts @@ -105,12 +105,12 @@ describe.each(regions)('Chat Nag Pack Tests | Region Test: %s', (awsRegion) => { //TODO Update expect values to remediate CDK NAG findings and remove debug test('AwsSolutions CDK NAG Warnings', () => { const warnings = Annotations.fromStack(stack).findWarning('*', Match.stringLikeRegexp('AwsSolutions-.*')); - expect(warnings.length).toBe(1); + expect(warnings.length).toBe(2); }); test('AwsSolutions CDK NAG Errors', () => { const errors = Annotations.fromStack(stack).findError('*', Match.stringLikeRegexp('AwsSolutions-.*')); - expect(errors.length).toBe(17); + expect(errors.length).toBe(28); }); test('NIST800.53r5 CDK NAG Warnings', () => { @@ -120,6 +120,6 @@ describe.each(regions)('Chat Nag Pack Tests | Region Test: %s', (awsRegion) => { test('NIST800.53r5 CDK NAG Errors', () => { const errors = Annotations.fromStack(stack).findError('*', Match.stringLikeRegexp('NIST.*')); - expect(errors.length).toBe(4); + expect(errors.length).toBe(11); }); }); From 74c85c22b6cd4b9517dae7ef9113bc75283cd143 Mon Sep 17 00:00:00 2001 From: bedanley Date: Tue, 5 Nov 2024 13:03:21 -0700 Subject: [PATCH 29/48] Update Docs gateway stage url to match github --- lib/docs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/index.ts b/lib/docs/index.ts index 74e7aa5f..4baab261 100644 --- a/lib/docs/index.ts +++ b/lib/docs/index.ts @@ -78,7 +78,7 @@ export class LisaDocsStack extends Stack { description: 'API Gateway for S3 hosted website', endpointConfiguration: { types: [EndpointType.REGIONAL] }, deployOptions: { - stageName: 'lisa', + stageName: 'LISA', }, binaryMediaTypes: ['*/*'], }); From 26902b792cbb73f04d7b8b5ddd349cc1a34ea0b9 Mon Sep 17 00:00:00 2001 From: bedanley Date: Wed, 6 Nov 2024 11:04:44 -0700 Subject: [PATCH 30/48] Add finch support (#174) --- Makefile | 19 +++++++++---------- README.md | 3 ++- package.json | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index c5129a00..6928b92c 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,8 @@ PROJECT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) HEADLESS = false - +DOCKER_CMD := $(CDK_DOCKER) +DOCKER_CMD ?= docker # Arguments defined through command line or config.yaml # PROFILE (optional argument) @@ -150,13 +151,11 @@ installTypeScriptRequirements: ## Make sure Docker is running dockerCheck: - @cmd_output=$$(docker ps); \ - if \ - [ $$? != 0 ]; \ - then \ - echo $$cmd_output; \ - exit 1; \ - fi; \ + @cmd_output=$$(pgrep -f "${DOCKER_CMD}"); \ + if [ $$? != 0 ]; then \ + echo "Process $(DOCKER_CMD) is not running. Exiting..."; \ + exit 1; \ + fi \ ## Check if models are uploaded modelCheck: @@ -229,11 +228,11 @@ cleanMisc: dockerLogin: dockerCheck ifdef PROFILE @$(foreach ACCOUNT,$(ACCOUNT_NUMBERS_ECR), \ - aws ecr get-login-password --region ${REGION} --profile ${PROFILE} | docker login --username AWS --password-stdin $(ACCOUNT).dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ + aws ecr get-login-password --region ${REGION} --profile ${PROFILE} | ${DOCKER_CMD} login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ ) else @$(foreach ACCOUNT,$(ACCOUNT_NUMBERS_ECR), \ - aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin $(ACCOUNT).dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ + aws ecr get-login-password --region ${REGION} | ${DOCKER_CMD} login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ ) endif diff --git a/README.md b/README.md index 78b4ee0f..2d7cadab 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Before beginning, ensure you have: 3. Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles 4. Python 3.9 or later 5. Node.js 14 or later -6. Docker installed and running +6. Docker/Finch installed and running 7. Sufficient disk space for model downloads and conversions If you're new to CDK, review the [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) and consult with your AWS support team. @@ -235,6 +235,7 @@ Set the following environment variables: export PROFILE=my-aws-profile # Optional, can be left blank export DEPLOYMENT_NAME=my-deployment export ENV=dev # Options: dev, test, or prod +export CDK_DOCKER=finch # Optional, only required if not using docker as container engine ``` --- diff --git a/package.json b/package.json index 41e648f2..f90ce33e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "migrate-properties": "node ./scripts/migrate-properties.mjs", "postinstall": "(cd lib/user-interface/react && npm install) && (cd lib/docs && npm install)", "postbuild": "(cd lib/user-interface/react && npm build) && (cd lib/docs && npm build)" -}, + }, "devDependencies": { "@aws-cdk/aws-lambda-python-alpha": "2.125.0-alpha.0", "@aws-sdk/client-iam": "^3.490.0", From 7cfc8d8dea55672b34ddb36e9aab3ac26fe5ac2e Mon Sep 17 00:00:00 2001 From: Dustin Sweigart Date: Thu, 7 Nov 2024 16:43:29 +0000 Subject: [PATCH 31/48] added metadata options for model launch templates --- .../src/lib/lisa_model_stack.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/ecs_model_deployer/src/lib/lisa_model_stack.ts b/ecs_model_deployer/src/lib/lisa_model_stack.ts index 8bc5f962..0d59814e 100644 --- a/ecs_model_deployer/src/lib/lisa_model_stack.ts +++ b/ecs_model_deployer/src/lib/lisa_model_stack.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import { Stack, StackProps } from 'aws-cdk-lib'; +import { Aspects, CfnResource, IAspect, Stack, StackProps } from 'aws-cdk-lib'; import { Vpc, SecurityGroup, Subnet, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; @@ -30,6 +30,28 @@ export type LisaModelStackProps = { modelConfig: ModelConfig; } & StackProps; +/** + * Modifies all AWS::EC2::LaunchTemplate resources in a CDK application. It directly adjusts the synthesized + * CloudFormation template, setting the HttpPutResponseHopLimit within MetadataOptions to 2 and HttpTokens to required. + */ +class UpdateLaunchTemplateMetadataOptions implements IAspect { + /** + * Checks if the given node is an instance of CfnResource and specifically an AWS::EC2::LaunchTemplate resource. + * If both conditions are true, it applies a direct override to the CloudFormation resource's properties, setting + * the HttpPutResponseHopLimit to 2 and HttpTokens to 'required'. + * + * @param {Construct} node - The CDK construct being visited. + */ + public visit (node: Construct): void { + // Check if the node is a CloudFormation resource of type AWS::EC2::LaunchTemplate + if (node instanceof CfnResource && node.cfnResourceType === 'AWS::EC2::LaunchTemplate') { + // Directly modify the CloudFormation properties to include the desired settings + node.addOverride('Properties.LaunchTemplateData.MetadataOptions.HttpPutResponseHopLimit', 2); + node.addOverride('Properties.LaunchTemplateData.MetadataOptions.HttpTokens', 'required'); + } + } +} + export class LisaModelStack extends Stack { constructor (scope: Construct, id: string, props: LisaModelStackProps) { super(scope, id, props); @@ -55,5 +77,7 @@ export class LisaModelStack extends Stack { vpc: vpc, subnetSelection: subnetSelection }); + + Aspects.of(this).add(new UpdateLaunchTemplateMetadataOptions()); } } From 5c8dd85e03ffd54e3af9fc4a8bb1331fe8591dc0 Mon Sep 17 00:00:00 2001 From: bedanley Date: Thu, 7 Nov 2024 10:32:22 -0700 Subject: [PATCH 32/48] Add finch support --- Makefile | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 6928b92c..92ec4518 100644 --- a/Makefile +++ b/Makefile @@ -11,8 +11,8 @@ PROJECT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) HEADLESS = false -DOCKER_CMD := $(CDK_DOCKER) -DOCKER_CMD ?= docker +DOCKER_CMD ?= $(or $(CDK_DOCKER),docker) + # Arguments defined through command line or config.yaml # PROFILE (optional argument) @@ -151,11 +151,12 @@ installTypeScriptRequirements: ## Make sure Docker is running dockerCheck: - @cmd_output=$$(pgrep -f "${DOCKER_CMD}"); \ + @cmd_output=$$($(DOCKER_CMD) ps); \ if [ $$? != 0 ]; then \ echo "Process $(DOCKER_CMD) is not running. Exiting..."; \ exit 1; \ - fi \ + fi; \ + ## Check if models are uploaded modelCheck: @@ -228,14 +229,15 @@ cleanMisc: dockerLogin: dockerCheck ifdef PROFILE @$(foreach ACCOUNT,$(ACCOUNT_NUMBERS_ECR), \ - aws ecr get-login-password --region ${REGION} --profile ${PROFILE} | ${DOCKER_CMD} login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ + aws ecr get-login-password --region ${REGION} --profile ${PROFILE} | $(DOCKER_CMD) login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ ) else @$(foreach ACCOUNT,$(ACCOUNT_NUMBERS_ECR), \ - aws ecr get-login-password --region ${REGION} | ${DOCKER_CMD} login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ + aws ecr get-login-password --region ${REGION} | $(DOCKER_CMD) login --username AWS --password-stdin ${ACCOUNT}.dkr.ecr.${REGION}.${URL_SUFFIX} >/dev/null 2>&1; \ ) endif + listStacks: @npx cdk list From 9ca5f0b6472619255e4aa25aa2034a008d6240ab Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 12 Nov 2024 12:31:30 -0700 Subject: [PATCH 33/48] Feature/vpc subnet updates --- lib/api-base/utils.ts | 40 ++++++- lib/networking/vpc/index.ts | 2 +- lib/rag/index.ts | 34 ++++-- lib/rag/layer/requirements.txt | 6 +- lib/serve/index.ts | 17 ++- package-lock.json | 212 +++++++++++++++++++++++++++++---- package.json | 3 +- 7 files changed, 269 insertions(+), 45 deletions(-) diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index 2422742d..e618c78e 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -34,12 +34,13 @@ import { IRestApi, Cors, } from 'aws-cdk-lib/aws-apigateway'; -import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup, ISubnet } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { Code, Function, Runtime, ILayerVersion, IFunction, CfnPermission } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; import { Vpc } from '../networking/vpc'; import { Queue } from 'aws-cdk-lib/aws-sqs'; +import * as AWS from 'aws-sdk'; /** * Type representing python lambda function @@ -156,3 +157,40 @@ function getOrCreateResource (scope: Construct, parentResource: IResource, path: } return resource; } + +export async function getSubnetCidrRange (subnet: string): Promise { + const ec2 = new AWS.EC2(); + try { + const describeSubnetsResponse = await ec2.describeSubnets({ + SubnetIds: [subnet], + }).promise(); + + const retrievedSubnet = describeSubnetsResponse.Subnets?.[0]; + + if (retrievedSubnet && retrievedSubnet.CidrBlock) { + return retrievedSubnet.CidrBlock; + } + } catch (error) { + console.error('Error retrieving subnet CIDR range:', error); + } + + return undefined; +} + +export async function isSubnetPublic (subnet: ISubnet): Promise { + const ec2 = new AWS.EC2(); + try { + const describeSubnetsResponse = await ec2.describeSubnets({ + SubnetIds: [subnet.subnetId], + }).promise(); + + const retrievedSubnet = describeSubnetsResponse.Subnets?.[0]; + + if (retrievedSubnet && retrievedSubnet.MapPublicIpOnLaunch) { + return true; + } + } catch (error) { + console.error('Error retrieving subnet CIDR range:', error); + } + return false; +} diff --git a/lib/networking/vpc/index.ts b/lib/networking/vpc/index.ts index 1f8436f1..72abd2f1 100644 --- a/lib/networking/vpc/index.ts +++ b/lib/networking/vpc/index.ts @@ -78,7 +78,7 @@ export class Vpc extends Construct { this, createCdkId([config.deploymentName, 'Imported-Subnets']), { - vpc: this.vpc, + vpc: vpc, description: 'This SubnetGroup is made up of imported Subnets via the deployment config', vpcSubnets: this.subnetSelection, } diff --git a/lib/rag/index.ts b/lib/rag/index.ts index 2db54b2d..602a2891 100644 --- a/lib/rag/index.ts +++ b/lib/rag/index.ts @@ -39,6 +39,7 @@ import { Layer } from '../core/layers'; import { createCdkId } from '../core/utils'; import { Vpc } from '../networking/vpc'; import { BaseProps, RagRepositoryType } from '../schema'; +import { getSubnetCidrRange, isSubnetPublic } from '../api-base/utils'; const HERE = path.resolve(__dirname); const RAG_LAYER_PATH = path.join(HERE, 'layer'); @@ -128,12 +129,17 @@ export class LisaRagStack extends Stack { description: 'Security group for RAG OpenSearch domain', }); // Allow communication from private subnets to ECS cluster - vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets).forEach((subnet) => { - openSearchSg.connections.allowFrom( - Peer.ipv4(subnet.ipv4CidrBlock), - Port.tcp(443), - 'Allow private subnets to communicate with OpenSearch cluster', - ); + const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); + subNets?.filter((subnet) => !isSubnetPublic(subnet)).forEach((subnet) => { + getSubnetCidrRange(subnet.subnetId).then((cidrRange) => { + if (cidrRange){ + openSearchSg.connections.allowFrom( + Peer.ipv4(cidrRange), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); + } + }); }); new CfnOutput(this, 'openSearchSg', { value: openSearchSg.securityGroupId }); @@ -251,12 +257,16 @@ export class LisaRagStack extends Stack { }); const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); - subNets?.forEach((subnet) => { - pgvectorSg.connections.allowFrom( - Peer.ipv4(subnet.ipv4CidrBlock), - Port.tcp(ragConfig.rdsConfig?.dbPort || 5432), - 'Allow private subnets to communicate with PGVector database', - ); + subNets?.filter((subnet) => !isSubnetPublic(subnet)).forEach((subnet) => { + getSubnetCidrRange(subnet.subnetId).then((cidrRange) => { + if (cidrRange){ + pgvectorSg.connections.allowFrom( + Peer.ipv4(cidrRange), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); + } + }); }); const username = ragConfig.rdsConfig.username; diff --git a/lib/rag/layer/requirements.txt b/lib/rag/layer/requirements.txt index d35e937e..88694e98 100644 --- a/lib/rag/layer/requirements.txt +++ b/lib/rag/layer/requirements.txt @@ -1,8 +1,8 @@ boto3>=1.34.131 botocore>=1.34.131 -langchain==0.3.0 -langchain-community==0.3.0 -langchain-openai==0.2.4 +langchain==0.2.16 +langchain-community==0.2.17 +langchain-openai==0.1.25 opensearch-py==2.6.0 pgvector==0.2.5 psycopg2-binary==2.9.9 diff --git a/lib/serve/index.ts b/lib/serve/index.ts index 29867f4d..a25b99c6 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -30,6 +30,7 @@ import { Vpc } from '../networking/vpc'; import { BaseProps } from '../schema'; import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { getSubnetCidrRange, isSubnetPublic } from '../api-base/utils'; const HERE = path.resolve(__dirname); @@ -148,12 +149,16 @@ export class LisaServeApplicationStack extends Stack { }); const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); - subNets?.forEach((subnet) => { - litellmDbSg.connections.allowFrom( - Peer.ipv4(subnet.ipv4CidrBlock), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database', - ); + subNets?.filter((subnet) => !isSubnetPublic(subnet)).forEach((subnet) => { + getSubnetCidrRange(subnet.subnetId).then((cidrRange) => { + if (cidrRange){ + litellmDbSg.connections.allowFrom( + Peer.ipv4(cidrRange), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); + } + }); }); const username = config.restApiConfig.rdsConfig.username; diff --git a/package-lock.json b/package-lock.json index 0664a897..948445c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "Apache-2.0", "dependencies": { "aws-cdk-lib": "2.125.0", + "aws-sdk": "^2.0.0", "cdk-nag": "^2.27.198", "constructs": "^10.0.0", "js-yaml": "^4.1.0", @@ -4234,7 +4235,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -4615,6 +4615,37 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4810,6 +4841,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -4890,16 +4941,32 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buffer/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5436,7 +5503,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5744,7 +5810,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -5756,7 +5821,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -6280,6 +6344,15 @@ "dev": true, "license": "MIT" }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6526,7 +6599,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -6556,7 +6628,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6623,7 +6694,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -6822,7 +6892,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -6864,7 +6933,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -6876,7 +6944,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6888,7 +6955,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -6900,7 +6966,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6915,7 +6980,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6965,6 +7029,12 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7041,8 +7111,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "1.3.8", @@ -7064,6 +7133,22 @@ "node": ">= 0.4" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -7118,7 +7203,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7202,6 +7286,21 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7336,7 +7435,6 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, "dependencies": { "which-typed-array": "^1.1.14" }, @@ -9197,6 +9295,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10271,7 +10378,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -10377,6 +10483,15 @@ } ] }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10645,6 +10760,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -10667,7 +10788,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -11412,6 +11532,35 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -11489,7 +11638,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", @@ -11592,6 +11740,28 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index f90ce33e..e55dc258 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "source-map-support": "^0.5.21", - "zod": "^3.22.3" + "zod": "^3.22.3", + "aws-sdk": "^2.0.0" }, "lint-staged": { "*.ts": [ From afad1a3a0e96343c5bc808b36fa8b905e27de149 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 12 Nov 2024 13:31:41 -0700 Subject: [PATCH 34/48] Update permissions on execution role --- lib/models/model-api.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/models/model-api.ts b/lib/models/model-api.ts index 82875943..d3c41c56 100644 --- a/lib/models/model-api.ts +++ b/lib/models/model-api.ts @@ -179,7 +179,13 @@ export class ModelsApi extends Construct { new PolicyStatement({ effect: Effect.ALLOW, actions: [ - 'ec2:TerminateInstances' + 'ec2:TerminateInstances', + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DescribeSubnets', + 'ec2:DeleteNetworkInterface', + 'ec2:AssignPrivateIpAddresses', + 'ec2:UnassignPrivateIpAddresses' ], resources: ['*'], conditions: { From c0a6dd66b85a217bb53ec1a284cc763e731014cb Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 12 Nov 2024 15:14:34 -0700 Subject: [PATCH 35/48] Update permissions on execution role --- lib/models/model-api.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/models/model-api.ts b/lib/models/model-api.ts index d3c41c56..cdb2224e 100644 --- a/lib/models/model-api.ts +++ b/lib/models/model-api.ts @@ -179,7 +179,6 @@ export class ModelsApi extends Construct { new PolicyStatement({ effect: Effect.ALLOW, actions: [ - 'ec2:TerminateInstances', 'ec2:CreateNetworkInterface', 'ec2:DescribeNetworkInterfaces', 'ec2:DescribeSubnets', @@ -188,6 +187,13 @@ export class ModelsApi extends Construct { 'ec2:UnassignPrivateIpAddresses' ], resources: ['*'], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ec2:TerminateInstances' + ], + resources: ['*'], conditions: { 'StringEquals': {'aws:ResourceTag/lisa_temporary_instance': 'true'} } From 7d804088312a0ce9bbfa3bf21796cd209199ad4e Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 12 Nov 2024 15:47:06 -0700 Subject: [PATCH 36/48] Update permissions on execution role --- lib/models/ecs-model-deployer.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/models/ecs-model-deployer.ts b/lib/models/ecs-model-deployer.ts index 09c06174..ad237fe6 100644 --- a/lib/models/ecs-model-deployer.ts +++ b/lib/models/ecs-model-deployer.ts @@ -16,7 +16,7 @@ import { Construct } from 'constructs'; import { DockerImageCode, DockerImageFunction, IFunction } from 'aws-cdk-lib/aws-lambda'; -import { Role, ServicePrincipal, ManagedPolicy, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Role, ServicePrincipal, ManagedPolicy, Policy, PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam'; import { Stack, Duration, Size } from 'aws-cdk-lib'; import { createCdkId } from '../core/utils'; @@ -43,6 +43,18 @@ export class ECSModelDeployer extends Construct { new PolicyStatement({ actions: ['sts:AssumeRole'], resources: ['arn:*:iam::*:role/cdk-*'] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DescribeSubnets', + 'ec2:DeleteNetworkInterface', + 'ec2:AssignPrivateIpAddresses', + 'ec2:UnassignPrivateIpAddresses' + ], + resources: ['*'], }) ] }); From eaa61abae53002aac8914dcf5400c704545ce746 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 12 Nov 2024 15:52:21 -0700 Subject: [PATCH 37/48] Update permissions on execution role --- lib/models/docker-image-builder.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/models/docker-image-builder.ts b/lib/models/docker-image-builder.ts index da2174d9..14b186b4 100644 --- a/lib/models/docker-image-builder.ts +++ b/lib/models/docker-image-builder.ts @@ -16,7 +16,15 @@ import { Construct } from 'constructs'; import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; -import { Role, InstanceProfile, ServicePrincipal, ManagedPolicy, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { + Role, + InstanceProfile, + ServicePrincipal, + ManagedPolicy, + Policy, + PolicyStatement, + Effect +} from 'aws-cdk-lib/aws-iam'; import { Stack, Duration } from 'aws-cdk-lib'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; @@ -91,7 +99,13 @@ export class DockerImageBuilder extends Construct { new PolicyStatement({ actions: [ 'ec2:RunInstances', - 'ec2:CreateTags' + 'ec2:CreateTags', + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DescribeSubnets', + 'ec2:DeleteNetworkInterface', + 'ec2:AssignPrivateIpAddresses', + 'ec2:UnassignPrivateIpAddresses' ], resources: ['*'] }), From 7d369065d17508c760a08cdd7f2ea0da42a5c249 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Tue, 12 Nov 2024 16:13:10 -0700 Subject: [PATCH 38/48] Update permissions on execution role --- lib/models/docker-image-builder.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/models/docker-image-builder.ts b/lib/models/docker-image-builder.ts index 14b186b4..98c05bd9 100644 --- a/lib/models/docker-image-builder.ts +++ b/lib/models/docker-image-builder.ts @@ -22,8 +22,7 @@ import { ServicePrincipal, ManagedPolicy, Policy, - PolicyStatement, - Effect + PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Stack, Duration } from 'aws-cdk-lib'; import { Bucket } from 'aws-cdk-lib/aws-s3'; From 137540c97e32825afd0716bb8438e9238c48022a Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 14 Nov 2024 11:24:25 -0700 Subject: [PATCH 39/48] Update subnet allocations --- lib/api-base/utils.ts | 24 +++--------------------- lib/rag/index.ts | 34 +++++++++++++++------------------- lib/serve/index.ts | 18 ++++++++---------- 3 files changed, 26 insertions(+), 50 deletions(-) diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index e618c78e..381084cd 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -34,7 +34,7 @@ import { IRestApi, Cors, } from 'aws-cdk-lib/aws-apigateway'; -import { ISecurityGroup, ISubnet } from 'aws-cdk-lib/aws-ec2'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { Code, Function, Runtime, ILayerVersion, IFunction, CfnPermission } from 'aws-cdk-lib/aws-lambda'; import { Construct } from 'constructs'; @@ -158,8 +158,8 @@ function getOrCreateResource (scope: Construct, parentResource: IResource, path: return resource; } -export async function getSubnetCidrRange (subnet: string): Promise { - const ec2 = new AWS.EC2(); +export async function getSubnetCidrRange (subnet: string, region: string): Promise { + const ec2 = new AWS.EC2({region: region}); try { const describeSubnetsResponse = await ec2.describeSubnets({ SubnetIds: [subnet], @@ -176,21 +176,3 @@ export async function getSubnetCidrRange (subnet: string): Promise { - const ec2 = new AWS.EC2(); - try { - const describeSubnetsResponse = await ec2.describeSubnets({ - SubnetIds: [subnet.subnetId], - }).promise(); - - const retrievedSubnet = describeSubnetsResponse.Subnets?.[0]; - - if (retrievedSubnet && retrievedSubnet.MapPublicIpOnLaunch) { - return true; - } - } catch (error) { - console.error('Error retrieving subnet CIDR range:', error); - } - return false; -} diff --git a/lib/rag/index.ts b/lib/rag/index.ts index 602a2891..b8052ce9 100644 --- a/lib/rag/index.ts +++ b/lib/rag/index.ts @@ -39,7 +39,7 @@ import { Layer } from '../core/layers'; import { createCdkId } from '../core/utils'; import { Vpc } from '../networking/vpc'; import { BaseProps, RagRepositoryType } from '../schema'; -import { getSubnetCidrRange, isSubnetPublic } from '../api-base/utils'; +import { getSubnetCidrRange } from '../api-base/utils'; const HERE = path.resolve(__dirname); const RAG_LAYER_PATH = path.join(HERE, 'layer'); @@ -130,15 +130,13 @@ export class LisaRagStack extends Stack { }); // Allow communication from private subnets to ECS cluster const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); - subNets?.filter((subnet) => !isSubnetPublic(subnet)).forEach((subnet) => { - getSubnetCidrRange(subnet.subnetId).then((cidrRange) => { - if (cidrRange){ - openSearchSg.connections.allowFrom( - Peer.ipv4(cidrRange), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database', - ); - } + subNets?.forEach((subnet) => { + getSubnetCidrRange(subnet.subnetId ?? subnet, config.region).then((cidrRange) => { + openSearchSg.connections.allowFrom( + Peer.ipv4(cidrRange ?? subnet.ipv4CidrBlock), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); }); }); new CfnOutput(this, 'openSearchSg', { value: openSearchSg.securityGroupId }); @@ -257,15 +255,13 @@ export class LisaRagStack extends Stack { }); const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); - subNets?.filter((subnet) => !isSubnetPublic(subnet)).forEach((subnet) => { - getSubnetCidrRange(subnet.subnetId).then((cidrRange) => { - if (cidrRange){ - pgvectorSg.connections.allowFrom( - Peer.ipv4(cidrRange), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database', - ); - } + subNets?.forEach((subnet) => { + getSubnetCidrRange(subnet.subnetId ?? subnet, config.region).then((cidrRange) => { + pgvectorSg.connections.allowFrom( + Peer.ipv4(cidrRange ?? subnet.ipv4CidrBlock), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); }); }); diff --git a/lib/serve/index.ts b/lib/serve/index.ts index a25b99c6..09f6e82d 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -30,7 +30,7 @@ import { Vpc } from '../networking/vpc'; import { BaseProps } from '../schema'; import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; -import { getSubnetCidrRange, isSubnetPublic } from '../api-base/utils'; +import { getSubnetCidrRange } from '../api-base/utils'; const HERE = path.resolve(__dirname); @@ -149,15 +149,13 @@ export class LisaServeApplicationStack extends Stack { }); const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); - subNets?.filter((subnet) => !isSubnetPublic(subnet)).forEach((subnet) => { - getSubnetCidrRange(subnet.subnetId).then((cidrRange) => { - if (cidrRange){ - litellmDbSg.connections.allowFrom( - Peer.ipv4(cidrRange), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database', - ); - } + subNets?.forEach((subnet) => { + getSubnetCidrRange(subnet.subnetId ?? subnet, config.region).then((cidrRange) => { + litellmDbSg.connections.allowFrom( + Peer.ipv4(cidrRange ?? subnet.ipv4CidrBlock), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database' + ); }); }); From 0baa69b9f5be292cb0296902bb4104a2728584b8 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 14 Nov 2024 13:24:38 -0700 Subject: [PATCH 40/48] Update Subnet and CIDR structure --- lib/api-base/utils.ts | 20 ---- lib/networking/vpc/index.ts | 4 +- lib/rag/index.ts | 29 ++--- lib/schema.ts | 5 +- lib/serve/index.ts | 15 +-- package-lock.json | 212 ++++-------------------------------- package.json | 3 +- 7 files changed, 46 insertions(+), 242 deletions(-) diff --git a/lib/api-base/utils.ts b/lib/api-base/utils.ts index 381084cd..2422742d 100644 --- a/lib/api-base/utils.ts +++ b/lib/api-base/utils.ts @@ -40,7 +40,6 @@ import { Code, Function, Runtime, ILayerVersion, IFunction, CfnPermission } from import { Construct } from 'constructs'; import { Vpc } from '../networking/vpc'; import { Queue } from 'aws-cdk-lib/aws-sqs'; -import * as AWS from 'aws-sdk'; /** * Type representing python lambda function @@ -157,22 +156,3 @@ function getOrCreateResource (scope: Construct, parentResource: IResource, path: } return resource; } - -export async function getSubnetCidrRange (subnet: string, region: string): Promise { - const ec2 = new AWS.EC2({region: region}); - try { - const describeSubnetsResponse = await ec2.describeSubnets({ - SubnetIds: [subnet], - }).promise(); - - const retrievedSubnet = describeSubnetsResponse.Subnets?.[0]; - - if (retrievedSubnet && retrievedSubnet.CidrBlock) { - return retrievedSubnet.CidrBlock; - } - } catch (error) { - console.error('Error retrieving subnet CIDR range:', error); - } - - return undefined; -} diff --git a/lib/networking/vpc/index.ts b/lib/networking/vpc/index.ts index 72abd2f1..89b608ce 100644 --- a/lib/networking/vpc/index.ts +++ b/lib/networking/vpc/index.ts @@ -69,9 +69,9 @@ export class Vpc extends Construct { // Checks if SubnetIds are provided in the config, if so we import them for use. // A VPC must be supplied if Subnets are being used. - if (config.subnetIds && config.subnetIds.length > 0) { + if (config.subnets && config.subnets.length > 0) { this.subnetSelection = { - subnets: props.config.subnetIds?.map((subnet, index) => Subnet.fromSubnetId(this, index.toString(), subnet)) + subnets: props.config.subnets?.map((subnet, index) => Subnet.fromSubnetId(this, index.toString(), subnet.subnetId)) }; this.subnetGroup = new SubnetGroup( diff --git a/lib/rag/index.ts b/lib/rag/index.ts index b8052ce9..6990d6bd 100644 --- a/lib/rag/index.ts +++ b/lib/rag/index.ts @@ -39,7 +39,6 @@ import { Layer } from '../core/layers'; import { createCdkId } from '../core/utils'; import { Vpc } from '../networking/vpc'; import { BaseProps, RagRepositoryType } from '../schema'; -import { getSubnetCidrRange } from '../api-base/utils'; const HERE = path.resolve(__dirname); const RAG_LAYER_PATH = path.join(HERE, 'layer'); @@ -129,15 +128,13 @@ export class LisaRagStack extends Stack { description: 'Security group for RAG OpenSearch domain', }); // Allow communication from private subnets to ECS cluster - const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); + const subNets = config.subnets && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); subNets?.forEach((subnet) => { - getSubnetCidrRange(subnet.subnetId ?? subnet, config.region).then((cidrRange) => { - openSearchSg.connections.allowFrom( - Peer.ipv4(cidrRange ?? subnet.ipv4CidrBlock), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database', - ); - }); + openSearchSg.connections.allowFrom( + Peer.ipv4(config.subnets ? config.subnets.filter((filteredSubnet) => filteredSubnet.subnetId === subnet.subnetId)?.[0]?.ipv4CidrBlock : subnet.ipv4CidrBlock), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); }); new CfnOutput(this, 'openSearchSg', { value: openSearchSg.securityGroupId }); @@ -254,15 +251,13 @@ export class LisaRagStack extends Stack { description: 'Security group for RAG PGVector database', }); - const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); + const subNets = config.subnets && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); subNets?.forEach((subnet) => { - getSubnetCidrRange(subnet.subnetId ?? subnet, config.region).then((cidrRange) => { - pgvectorSg.connections.allowFrom( - Peer.ipv4(cidrRange ?? subnet.ipv4CidrBlock), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database', - ); - }); + pgvectorSg.connections.allowFrom( + Peer.ipv4(config.subnets ? config.subnets.filter((filteredSubnet) => filteredSubnet.subnetId === subnet.subnetId)?.[0]?.ipv4CidrBlock : subnet.ipv4CidrBlock), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); }); const username = ragConfig.rdsConfig.username; diff --git a/lib/schema.ts b/lib/schema.ts index d644874e..6473edba 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -714,7 +714,10 @@ const RawConfigSchema = z region: z.string(), restApiConfig: FastApiContainerConfigSchema, vpcId: z.string().optional(), - subnetIds: z.array(z.string().startsWith('subnet-')).optional(), + subnets: z.array(z.object({ + subnetId: z.string().startsWith('subnet-'), + ipv4CidrBlock: z.string() + })).optional(), deploymentStage: z.string().default('prod'), removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]).transform((value) => REMOVAL_POLICIES[value]).default('destroy'), runCdkNag: z.boolean().default(false), diff --git a/lib/serve/index.ts b/lib/serve/index.ts index 09f6e82d..f8f35def 100644 --- a/lib/serve/index.ts +++ b/lib/serve/index.ts @@ -30,7 +30,6 @@ import { Vpc } from '../networking/vpc'; import { BaseProps } from '../schema'; import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; -import { getSubnetCidrRange } from '../api-base/utils'; const HERE = path.resolve(__dirname); @@ -148,15 +147,13 @@ export class LisaServeApplicationStack extends Stack { description: 'Security group for LiteLLM dynamic model management database.', }); - const subNets = config.subnetIds && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); + const subNets = config.subnets && config.vpcId ? vpc.subnetSelection?.subnets : vpc.vpc.isolatedSubnets.concat(vpc.vpc.privateSubnets); subNets?.forEach((subnet) => { - getSubnetCidrRange(subnet.subnetId ?? subnet, config.region).then((cidrRange) => { - litellmDbSg.connections.allowFrom( - Peer.ipv4(cidrRange ?? subnet.ipv4CidrBlock), - Port.tcp(config.restApiConfig.rdsConfig.dbPort), - 'Allow REST API private subnets to communicate with LiteLLM database' - ); - }); + litellmDbSg.connections.allowFrom( + Peer.ipv4(config.subnets ? config.subnets.filter((filteredSubnet) => filteredSubnet.subnetId === subnet.subnetId)?.[0]?.ipv4CidrBlock : subnet.ipv4CidrBlock), + Port.tcp(config.restApiConfig.rdsConfig.dbPort), + 'Allow REST API private subnets to communicate with LiteLLM database', + ); }); const username = config.restApiConfig.rdsConfig.username; diff --git a/package-lock.json b/package-lock.json index 948445c9..0664a897 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "license": "Apache-2.0", "dependencies": { "aws-cdk-lib": "2.125.0", - "aws-sdk": "^2.0.0", "cdk-nag": "^2.27.198", "constructs": "^10.0.0", "js-yaml": "^4.1.0", @@ -4235,6 +4234,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -4615,37 +4615,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/aws-sdk": { - "version": "2.1692.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", - "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-sdk/node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4841,26 +4810,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -4941,32 +4890,16 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, - "node_modules/buffer/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5503,6 +5436,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5810,6 +5744,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -5821,6 +5756,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -6344,15 +6280,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6599,6 +6526,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -6628,6 +6556,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6694,6 +6623,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -6892,6 +6822,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -6933,6 +6864,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -6944,6 +6876,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -6955,6 +6888,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -6966,6 +6900,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6980,6 +6915,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -7029,12 +6965,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7111,7 +7041,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/ini": { "version": "1.3.8", @@ -7133,22 +7064,6 @@ "node": ">= 0.4" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -7203,6 +7118,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -7286,21 +7202,6 @@ "node": ">=6" } }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -7435,6 +7336,7 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, "dependencies": { "which-typed-array": "^1.1.14" }, @@ -9295,15 +9197,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10378,6 +10271,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -10483,15 +10377,6 @@ } ] }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10760,12 +10645,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", - "license": "ISC" - }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -10788,6 +10667,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -11532,35 +11412,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "license": "MIT", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "license": "MIT" - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -11638,6 +11489,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", @@ -11740,28 +11592,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index e55dc258..f90ce33e 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,7 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "source-map-support": "^0.5.21", - "zod": "^3.22.3", - "aws-sdk": "^2.0.0" + "zod": "^3.22.3" }, "lint-staged": { "*.ts": [ From 71123105e6a6e2c8d5c53133183022a75179ee2f Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Thu, 14 Nov 2024 13:39:33 -0700 Subject: [PATCH 41/48] Include subnet changes in model deployer --- ecs_model_deployer/src/lib/lisa_model_stack.ts | 4 ++-- ecs_model_deployer/src/lib/schema.ts | 5 ++++- lib/models/ecs-model-deployer.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ecs_model_deployer/src/lib/lisa_model_stack.ts b/ecs_model_deployer/src/lib/lisa_model_stack.ts index 0d59814e..01b96e7b 100644 --- a/ecs_model_deployer/src/lib/lisa_model_stack.ts +++ b/ecs_model_deployer/src/lib/lisa_model_stack.ts @@ -62,9 +62,9 @@ export class LisaModelStack extends Stack { let subnetSelection: SubnetSelection | undefined; - if (props.config.subnetIds && props.config.subnetIds.length > 0) { + if (props.config.subnets && props.config.subnets.length > 0) { subnetSelection = { - subnets: props.config.subnetIds?.map((subnet, index) => Subnet.fromSubnetId(this, index.toString(), subnet)) + subnets: props.config.subnets?.map((subnet, index) => Subnet.fromSubnetId(this, index.toString(), subnet.subnetId)) }; } diff --git a/ecs_model_deployer/src/lib/schema.ts b/ecs_model_deployer/src/lib/schema.ts index fda17ba9..11e3c5ec 100644 --- a/ecs_model_deployer/src/lib/schema.ts +++ b/ecs_model_deployer/src/lib/schema.ts @@ -618,7 +618,10 @@ const RawConfigSchema = z instanceProfilePrefix: z.string().optional(), }) .optional(), - subnetIds: z.array(z.string()).optional(), + subnets: z.array(z.object({ + subnetId: z.string().startsWith('subnet-'), + ipv4CidrBlock: z.string() + })).optional(), }) .refine((config) => (config.pypiConfig.indexUrl && config.region.includes('iso')) || !config.region.includes('iso'), { message: 'Must set PypiConfig if in an iso region', diff --git a/lib/models/ecs-model-deployer.ts b/lib/models/ecs-model-deployer.ts index ad237fe6..27a0866f 100644 --- a/lib/models/ecs-model-deployer.ts +++ b/lib/models/ecs-model-deployer.ts @@ -71,7 +71,7 @@ export class ECSModelDeployer extends Construct { 's3BucketModels': props.config.s3BucketModels, 'mountS3DebUrl': props.config.mountS3DebUrl, 'permissionsBoundaryAspect': props.config.permissionsBoundaryAspect, - 'subnetIds': props.config.subnetIds + 'subnets': props.config.subnets }; const functionId = createCdkId([stackName, 'ecs_model_deployer']); From effca3aafa248023d4245e0c2890bc7e953c7d1f Mon Sep 17 00:00:00 2001 From: bedanley Date: Thu, 14 Nov 2024 14:33:29 -0700 Subject: [PATCH 42/48] Generate Configuration Documentation --- README.md | 1386 +---------------- ecs_model_deployer/src/lib/ecs-model.ts | 2 +- lib/docs/.gitignore | 1 + lib/docs/.vitepress/config.mts | 20 +- lib/docs/admin/api.md | 364 ----- lib/docs/admin/architecture.md | 87 +- lib/docs/admin/components.md | 55 - lib/docs/admin/getting-started.md | 47 +- lib/docs/admin/idp.md | 1 - lib/docs/admin/model-management.md | 365 ++++- lib/docs/admin/overview.md | 63 + lib/docs/admin/security.md | 1 - lib/docs/config/api-tokens.md | 77 - lib/docs/config/branding.md | 14 +- lib/docs/config/configuration.md | 15 + lib/docs/config/features.md | 1 - lib/docs/config/idp.md | 35 + lib/docs/{admin => config}/lite-llm.md | 0 lib/docs/config/model-compatibility.md | 8 +- lib/docs/config/model-management-api.md | 1 - lib/docs/package.json | 3 +- lib/docs/user/breaking-changes.md | 63 + lib/docs/user/chat.md | 11 +- .../hiding-chat-components.md | 0 .../{config => user}/model-management-ui.md | 0 lib/schema.ts | 561 +++---- lib/stages.ts | 2 +- lib/zod2md.config.ts | 24 + package-lock.json | 460 +++++- package.json | 6 +- 30 files changed, 1409 insertions(+), 2264 deletions(-) delete mode 100644 lib/docs/admin/api.md delete mode 100644 lib/docs/admin/components.md delete mode 100644 lib/docs/admin/idp.md create mode 100644 lib/docs/admin/overview.md delete mode 100644 lib/docs/admin/security.md delete mode 100644 lib/docs/config/api-tokens.md create mode 100644 lib/docs/config/configuration.md delete mode 100644 lib/docs/config/features.md create mode 100644 lib/docs/config/idp.md rename lib/docs/{admin => config}/lite-llm.md (100%) delete mode 100644 lib/docs/config/model-management-api.md create mode 100644 lib/docs/user/breaking-changes.md rename lib/docs/{config => user}/hiding-chat-components.md (100%) rename lib/docs/{config => user}/model-management-ui.md (100%) create mode 100644 lib/zod2md.config.ts diff --git a/README.md b/README.md index 2d7cadab..c7253a2a 100644 --- a/README.md +++ b/README.md @@ -1,1310 +1,80 @@ -[![Full Documentation](https://img.shields.io/badge/Full%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/LISA/) - # LLM Inference Solution for Amazon Dedicated Cloud (LISA) -![LISA Architecture](./assets/LisaArchitecture.png) -LISA is an infrastructure-as-code solution that supports model hosting and inference. Customers deploy LISA directly -into an AWS account and provision their own infrastructure. Customers bring their own models to LISA for hosting and -inference through Amazon ECS. LISA accelerates the use of Generative AI (GenAI) applications by providing scalable, -low latency access to customers’ generative LLMs and embedding language models. Customers can then focus on -experimenting with LLMs and developing GenAI applications. - -LISA’s chatbot user interface can be used for experiment with features and for production use cases. LISA enhances model -output by integrating retrieval-augmented generation (RAG) with Amazon OpenSearch or PostgreSQL’s PGVector extension, -incorporating external knowledge sources into model responses. This helps reduce the need for fine-tuning and delivers -more contextually relevant outputs. - -LISA supports OpenAI’s API Spec via the LiteLLM proxy. This means that LISA is compatible for customers to configure -with models hosted externally by supported model providers. LiteLLM also allows customers to use LISA to standardize -model orchestration and communication across model providers instead of managing each individually. With OpenAI API spec -support, LISA can also be used as a stand-in replacement for any application that already utilizes OpenAI-centric -tooling (ex: OpenAI’s Python library, LangChain). - ---- -# Table of Contents - -- [LISA (LLM Inference Solution for Amazon Dedicated Cloud)](#lisa-llm-inference-solution-for-amazon-dedicated-cloud) -- [Breaking Changes in v2 to v3 Migration](#breaking-changes-in-v2-to-v3-migration) -- [Background](#background) -- [System Overview](#system-overview) -- [LISA Components](#lisa-components) - - [LISA Model Management](#lisa-model-management) - - [LISA Serve](#lisa-serve) - - [LISA Chat](#lisa-chat) -- [Interaction Flow](#interaction-flow) -- [Getting Started with LISA](#getting-started-with-lisa) - - [Prerequisites](#prerequisites) - - [Step 1: Clone the Repository](#step-1-clone-the-repository) - - [Step 2: Set Up Environment Variables](#step-2-set-up-environment-variables) - - [Step 3: Set Up Python and TypeScript Environments](#step-3-set-up-python-and-typescript-environments) - - [Step 4: Configure LISA](#step-4-configure-lisa) - - [Step 5: Stage Model Weights](#step-5-stage-model-weights) - - [Step 6: Configure Identity Provider](#step-6-configure-identity-provider) - - [Step 7: Configure LiteLLM](#step-7-configure-litellm) - - [Step 8: Set Up SSL Certificates (Development Only)](#step-8-set-up-ssl-certificates-development-only) - - [Step 9: Customize Model Deployment](#step-9-customize-model-deployment) - - [Step 10: Bootstrap CDK (If Not Already Done)](#step-10-bootstrap-cdk-if-not-already-done) -- [Recommended LiteLLM Configuration Options](#recommended-litellm-configuration-options) -- [API Usage Overview](#api-usage-overview) - - [User-facing OpenAI-Compatible API](#user-facing-openai-compatible-api) - - [Admin-level Model Management API](#admin-level-model-management-api) -- [Error Handling for API Requests](#error-handling-for-api-requests) -- [Deployment](#deployment) - - [Using Pre-built Resources](#using-pre-built-resources) - - [Deploying](#deploying) -- [Programmatic API Tokens](#programmatic-api-tokens) -- [Model Compatibility](#model-compatibility) -- [Chatbot Example](#chatbot-example) -- [Usage and Features](#usage-and-features) - - [OpenAI Specification Compatibility](#openai-specification-compatibility) - - [Continue JetBrains and VS Code Plugin](#continue-jetbrains-and-vs-code-plugin) - - [Usage in LLM Libraries](#usage-in-llm-libraries) -- [License Notice](#license-notice) - ---- -# Breaking Changes - -## v2 to v3 Migration - -With the release of LISA v3.0.0, we have introduced several architectural changes that are incompatible with previous versions. Although these changes may cause some friction for existing users, they aim to simplify the deployment experience and enhance long-term scalability. The following breaking changes are critical for existing users planning to upgrade: - -1. Model Deletion Upon Upgrade: Models deployed via EC2 and ECS using the config.yaml file’s ecsModels list will be deleted during the upgrade process. LISA has migrated to a new model deployment system that manages models internally, rendering the ecsModels list obsolete. We recommend backing up your model settings to facilitate their redeployment through the new Model Management API with minimal downtime. -1. Networking Changes and Full Teardown: Core networking changes require a complete teardown of the existing LISA installation using the make destroy command before upgrading. Cross-stack dependencies have been modified, necessitating this full teardown to ensure proper application of the v3 infrastructure changes. Additionally, users may need to manually delete some resources, such as ECR repositories or S3 buckets, if they were populated before CloudFormation began deleting the stack. This operation is destructive and irreversible, so it is crucial to back up any critical configurations and data (e.g., S3 RAG bucket contents, DynamoDB token tables) before proceeding with the upgrade. -1. New LiteLLM Admin Key Requirement: The new Model Management API requires an "admin" key for LiteLLM to track models for inference requests. This key, while transparent to users, must be present and conform to the required format (starting with sk-). The key is defined in the config.yaml file, and the LISA schema validator will prompt an error if it is missing or incorrectly formatted. - -## v3.0.0 to v3.1.0 - -In preparation of the v3.1.0 release, there are several changes that we needed to make in order to ensure the stability of the LISA system. -1. The CreateModel API `containerConfig` object has been changed so that the Docker Image repository is listed in `containerConfig.image.baseImage` instead of - its previous location at `containerConfig.baseImage.baseImage`. This change makes the configuration consistent with the config.yaml file in LISA v2.0 and prior. -2. The CreateModel API `containerConfig.image` object no longer requires the `path` option. We identified that this was a confusing and redundant option to set, considering - that the path was based on the LISA code repository structure, and that we already had an option to specify if a model was using TGI, TEI, or vLLM. Specifying the `inferenceContainer` - is sufficient for the system to infer which files to use so that the user does not have to provide this information. -3. The ApiDeployment stack now follows the same naming convention as the rest of the stacks that we deploy, utilization the deployment name and the deploymentStage names. This allows users - to have multiple LISA installations with different parameters in the same account without needing to change region or account entirely. After successful deployment, you may safely delete the - previous `${deploymentStage}-LisaApiDeployment` stack, as it is no longer in use. -4. If you have installed v3.0.0 or v3.0.1, you will need to **delete** the Models API stack so that the model deployer function will deploy again. The function was converted to a Docker Image - Function so that the growing Function size would fit within the Lambda constraints. We recommend that you take the following actions to avoid leaked resources: - 1. Use the Model Management UI to **delete all models** from LISA. This is needed so that we delete any CloudFormation stacks that track GPU instances. Failure to do this will require manual - resource cleanup to rid the account of inaccessible EC2 instances. Once the Models DynamoDB Table is deleted, we do not have a programmatic way to re-reference deployed models, so that is - why we recommend deleting them first. - 2. **Only after deleting all models through the Model Management UI**, manually delete the Model Management API stack in CloudFormation. This will take at least 45 minutes due to Lambda's use - of Elastic Network Interfaces for VPC access. The stack name will look like: `${deployment}-lisa-models-${deploymentStage}`. - 3. After the stack has been deleted, deploy LISA v3.1.0, which will recreate the Models API stack, along with the Docker Lambda Function. -5. The `ecsModels` section of `config.yaml` has been stripped down to only 3 fields per model: `modelName`, `inferenceContainer`, and `baseImage`. Just as before, the system will check to see if the models - defined here exist in your models S3 bucket prior to LISA deployment. These values will be needed later when invoking the Model Management API to create a model. ---- - -## Background - -LISA is a robust, AWS-native platform designed to simplify the deployment and management of Large Language Models (LLMs) in scalable, secure, and highly available environments. Drawing inspiration from the AWS open-source project [aws-genai-llm-chatbot](https://github.com/aws-samples/aws-genai-llm-chatbot), LISA builds on this foundation by offering more specialized functionality, particularly in the areas of security, modularity, and flexibility. - -One of the key differentiators of LISA is its ability to leverage the [text-generation-inference](https://github.com/huggingface/text-generation-inference/tree/main) text-generation-inference container from HuggingFace, allowing users to deploy cutting-edge LLMs. LISA also introduces several innovations that extend beyond its inspiration: - -1. **Support for Amazon Dedicated Cloud (ADC):** LISA is designed to operate in highly controlled environments like Amazon Dedicated Cloud (ADC) partitions, making it ideal for industries with stringent regulatory and security requirements. This focus on secure, isolated deployments differentiates LISA from other open-source platforms. -1. **Modular Design for Composability:** LISA's architecture is designed to be composable, splitting its components into distinct services. The core components, LISA Serve (for LLM serving and inference) and LISA Chat (for the chat interface), can be deployed as independent stacks. This modularity allows users to deploy only the parts they need, enhancing flexibility and scalability across different deployment environments. -1. **OpenAI API Specification Support:** LISA is built to support the OpenAI API specification, allowing users to replace OpenAI’s API with LISA without needing to change existing application code. This makes LISA a drop-in replacement for any workflow or application that already leverages OpenAI’s tooling, such as the OpenAI Python library or LangChain. - ---- - -## System Overview - -LISA is designed using a modular, microservices-based architecture, where each service performs a distinct function. It is composed of three core components: LISA Model Management, LISA Serve, and LISA Chat. Each of these components is responsible for specific functionality and interacts via well-defined API endpoints to ensure scalability, security, and fault tolerance across the system. - -**Key System Functionalities:** - -* **Authentication and Authorization** via AWS Cognito or OpenID Connect (OIDC) providers, ensuring secure access to both the REST API and Chat UI through token-based authentication and role-based access control. -* **Model Hosting** on AWS ECS with autoscaling and efficient traffic management using Application Load Balancers (ALBs), providing scalable and high-performance model inference. -* **Model Management** using AWS Step Functions to orchestrate complex workflows for creating, updating, and deleting models, automatically managing underlying ECS infrastructure. -* **Inference Requests** served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. -* **Chat Interface** enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. -* **Retrieval-Augmented Generation (RAG) Operations**, leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. - ---- - -## LISA Components - -### LISA Model Management -![LISA Model Management Architecture](./assets/LisaModelManagement.png) -The Model Management component is responsible for managing the entire lifecycle of models in LISA. This includes creation, updating, deletion, and scaling of models deployed on ECS. The system automates and scales these operations, ensuring that the underlying infrastructure is managed efficiently. - -* **Model Hosting**: Models are containerized and deployed on AWS ECS, with each model hosted in its own isolated ECS task. This design allows models to be independently scaled based on demand. Traffic to the models is balanced using Application Load Balancers (ALBs), ensuring that the autoscaling mechanism reacts to load fluctuations in real time, optimizing both performance and availability. -* **External Model Routing**: LISA utilizes the LiteLLM proxy to route traffic to different model providers, no matter their API and payload format. Users may add models from external providers, such as SageMaker or Bedrock, to their system to allow requests to models hosted in those systems and services. LISA will simply add the configuration to LiteLLM without creating any additional supporting infrastructure. -* **Model Lifecycle Management**: AWS Step Functions are used to orchestrate the lifecycle of models, handling the creation, update, and deletion workflows. Each workflow provisions the required resources using CloudFormation templates, which manage infrastructure components like EC2 instances, security groups, and ECS services. The system ensures that the necessary security, networking, and infrastructure components are automatically deployed and configured. - * The CloudFormation stacks define essential resources using the LISA core VPC configuration, ensuring best practices for security and access across all resources in the environment. - * DynamoDB stores model metadata, while Amazon S3 securely manages model weights, enabling ECS instances to retrieve the weights dynamically during deployment. - -#### Technical Implementation - -* **Model Lifecycle**: Lifecycle operations such as creation, update, and deletion are executed by Step Functions and backed by AWS Lambda in ```lambda/models/lambda_functions.py```. -* **CloudFormation**: Infrastructure components are provisioned using CloudFormation templates, as defined in ```ecs_model_deployer/src/lib/lisa_model_stack.ts```. -* **ECS Cluster**: ECS cluster and task definitions are located in ```ecs_model_deployer/src/lib/ecsCluster.ts```, with model containers specified in ```ecs_model_deployer/src/lib/ecs-model.ts```. - ---- - -### LISA Serve -![LISA Serve Architecture](./assets/LisaServe.png) -LISA Serve is responsible for processing inference requests and serving model predictions. This component manages user requests to interact with LLMs and ensures that the models deliver low-latency responses. - -* **Inference Requests**: Requests are routed via ALB, which serves as the main entry point to LISA’s backend infrastructure. The ALB forwards requests to the appropriate ECS-hosted model or externally-hosted model based on the request parameters. For models hosted within LISA, traffic to the models is managed with model-specific ALBs, which enable autoscaling if the models are under heavy load. LISA supports both direct REST API-based interaction and interaction through the Chat UI, enabling programmatic access or a user-friendly chat experience. -* **RAG (Retrieval-Augmented Generation)**: RAG operations enhance model responses by integrating external data sources. LISA leverages OpenSearch or PGVector (PostgreSQL) as vector stores, enabling vector-based search and retrieval of relevant knowledge to augment LLM outputs dynamically. - -#### Technical Implementation - -* RAG operations are managed through ```lambda/rag/lambda_functions.py```, which handles embedding generation and document retrieval via OpenSearch and PostgreSQL. -* Direct requests to the LISA Serve ALB entrypoint must utilize the OpenAI API spec, which we support through the use of the LiteLLM proxy. - ---- - -### LISA Chat -![LISA Chatbot Architecture](./assets/LisaChat.png) -LISA Chat provides a customizable chat interface that enables users to interact with models in real-time. This component ensures that users have a seamless experience for submitting queries and maintaining session continuity. - -* **Chat Interface**: The Chat UI is hosted as a static website on Amazon S3 and is served via API Gateway. Users can interact with models directly through the web-based frontend, sending queries and viewing real-time responses from the models. The interface is integrated with LISA's backend services for model inference, retrieval augmented generation, and session management. -* **Session History Management**: LISA maintains session histories using DynamoDB, allowing users to retrieve and continue previous conversations seamlessly. This feature is crucial for maintaining continuity in multi-turn conversations with the models. - -#### Technical Implementation - -* The Chat UI is implemented in the ```lib/user-interface/react/``` folder and is deployed using the scripts in the ```scripts/``` folder. -* Session management logic is handled in ```lambda/session/lambda_functions.py```, where session data is stored and retrieved from DynamoDB. -* RAG operations are defined in lambda/repository/lambda_functions.py - ---- - -## Interaction Flow - -1. **User Interaction with Chat UI or API:** Users can interact with LISA through the Chat UI or REST API. Each interaction is authenticated using AWS Cognito or OIDC, ensuring secure access. -1. **Request Routing:** The API Gateway securely routes user requests to the appropriate backend services, whether for fetching the chat UI, performing RAG operations, or managing models. -1. **Model Management:** Administrators can deploy, update, or delete models via the Model Management API, which triggers ECS deployment and scaling workflows. -1. **Model Inference:** Inference requests are routed to ECS-hosted models or external models via the LiteLLM proxy. Responses are served back to users through the ALB. -1. **RAG Integration:** When RAG is enabled, LISA retrieves relevant documents from OpenSearch or PGVector, augmenting the model's response with external knowledge. -1. **Session Continuity:** User session data is stored in DynamoDB, ensuring that users can retrieve and continue previous conversations across multiple interactions. -1. **Autoscaling:** ECS tasks automatically scale based on system load, with ALBs distributing traffic across available instances to ensure performance. - ---- - -# Getting Started with LISA - -LISA (LLM Inference Solution for Amazon Dedicated Cloud) is an advanced infrastructure solution for deploying and -managing Large Language Models (LLMs) on AWS. This guide will walk you through the setup process, from prerequisites -to deployment. - -## Prerequisites - -Before beginning, ensure you have: - -1. An AWS account with appropriate permissions. - 1. Because of all the resource creation that happens as part of CDK deployments, we expect Administrator or Administrator-like permissions with resource creation and mutation permissions. - Installation will not succeed if this profile does not have permissions to create and edit arbitrary resources for the system. - **Note**: This level of permissions is not required for the runtime of LISA, only its deployment and subsequent updates. -2. AWS CLI installed and configured -3. Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles -4. Python 3.9 or later -5. Node.js 14 or later -6. Docker/Finch installed and running -7. Sufficient disk space for model downloads and conversions - -If you're new to CDK, review the [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) and consult with your AWS support team. - -> [!TIP] -> To minimize version conflicts and ensure a consistent deployment environment, it is recommended to execute the following steps on a dedicated EC2 instance. However, LISA can be deployed from any machine that meets the prerequisites listed above. - ---- - -## Step 1: Clone the Repository - -Ensure you're working with the latest stable release of LISA: - -```bash -git clone -b main --single-branch -cd lisa -``` - ---- - -## Step 2: Set Up Environment Variables - -Create and configure your `config.yaml` file: - -```bash -cp example_config.yaml config.yaml -``` - -Set the following environment variables: - -```bash -export PROFILE=my-aws-profile # Optional, can be left blank -export DEPLOYMENT_NAME=my-deployment -export ENV=dev # Options: dev, test, or prod -export CDK_DOCKER=finch # Optional, only required if not using docker as container engine -``` - ---- - -## Step 3: Set Up Python and TypeScript Environments - -Install system dependencies and set up both Python and TypeScript environments: - -```bash -# Install system dependencies -sudo apt-get update -sudo apt-get install -y jq - -# Install Python packages -pip3 install --user --upgrade pip -pip3 install yq huggingface_hub s5cmd - -# Set up Python environment -make createPythonEnvironment - -# Activate your python environment -# The command is the output from the previous make command) - -# Install Python Requirements -make installPythonRequirements - -# Set up TypeScript environment -make createTypeScriptEnvironment -make installTypeScriptRequirements -``` - ---- - -## Step 4: Configure LISA - -Edit the `config.yaml` file to customize your LISA deployment. Key configurations include: - -- AWS account and region settings -- Model configurations -- Authentication settings -- Networking and infrastructure preferences - ---- - -## Step 5: Stage Model Weights - -LISA requires model weights to be staged in the S3 bucket specified in your `config.yaml` file, assuming the S3 bucket follows this structure: - -``` -s3:/// -s3://// -s3://// -... -s3:/// -``` - -**Example:** - -``` -s3:///mistralai/Mistral-7B-Instruct-v0.2 -s3:///mistralai/Mistral-7B-Instruct-v0.2/ -s3:///mistralai/Mistral-7B-Instruct-v0.2/ -... -``` - -To automatically download and stage the model weights defined by the `ecsModels` parameter in your `config.yaml`, use the following command: - -```bash -make modelCheck -``` - -This command verifies if the model's weights are already present in your S3 bucket. If not, it downloads the weights, converts them to the required format, and uploads them to your S3 bucket. Ensure adequate disk space is available for this process. - -> **WARNING** -> As of LISA 3.0, the `ecsModels` parameter in `config.yaml` is solely for staging model weights in your S3 bucket. Previously, before models could be managed through the [API](https://github.com/awslabs/LISA/blob/develop/README.md#creating-a-model-admin-api) or via the Model Management section of the [Chatbot](https://github.com/awslabs/LISA/blob/develop/README.md#chatbot-example), this parameter also dictated which models were deployed. - -> **NOTE** -> For air-gapped systems, before running `make modelCheck` you should manually download model artifacts and place them in a `models` directory at the project root, using the structure: `models/`. - -> **NOTE** -> This process is primarily designed and tested for HuggingFace models. For other model formats, you will need to manually create and upload safetensors. - ---- - -## Step 6: Configure Identity Provider - -In the `config.yaml` file, configure the `authConfig` block for authentication. LISA supports OpenID Connect (OIDC) providers such as AWS Cognito or Keycloak. Required fields include: - -- `authority`: URL of your identity provider -- `clientId`: Client ID for your application -- `adminGroup`: Group name for users with model management permissions -- `jwtGroupsProperty`: Path to the groups field in the JWT token -- `additionalScopes` (optional): Extra scopes for group membership information - -#### Cognito Configuration Example: -In Cognito, the `authority` will be the URL to your User Pool. As an example, if your User Pool ID, not the name, is `us-east-1_example`, and if it is -running in `us-east-1`, then the URL to put in the `authority` field would be `https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example`. The `clientId` -can be found in your User Pool's "App integration" tab from within the AWS Management Console, and at the bottom of the page, you will see the list of clients -and their associated Client IDs. The ID here is what we will need for the `clientId` field. - - -```yaml -authConfig: - authority: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example - clientId: your-client-id - adminGroup: AdminGroup - jwtGroupsProperty: cognito:groups -``` - -#### Keycloak Configuration Example: -In Keycloak, the `authority` will be the URL to your Keycloak server. The `clientId` is likely not a random string like in the Cognito clients, and instead -will be a string configured by your Keycloak administrator. Your administrator will be able to give you a client name or create a client for you to use for -this application. Once you have this string, use that as the `clientId` within the `authConfig` block. - -```yaml -authConfig: - authority: https://your-keycloak-server.com - clientId: your-client-name - adminGroup: AdminGroup - jwtGroupsProperty: realm_access.roles -``` - ---- - -## Step 7: Configure LiteLLM -We utilize LiteLLM under the hood to allow LISA to respond to the [OpenAI specification](https://platform.openai.com/docs/api-reference). -For LiteLLM configuration, a key must be set up so that the system may communicate with a database for tracking all the models that are added or removed -using the [Model Management API](#admin-level-model-management-api). The key must start with `sk-` and then can be any arbitrary string. We recommend generating a new UUID and then using that as -the key. Configuration example is below. - - -```yaml -litellmConfig: - general_settings: - master_key: sk-00000000-0000-0000-0000-000000000000 # needed for db operations, create your own key # pragma: allowlist-secret - model_list: [] -``` - -**Note**: It is possible to add LiteLLM-only models to this configuration, but it is not recommended as the models in this configuration will not show in the -Chat or Model Management UIs. Instead, use the [Model Management UI](#admin-level-model-management-api) to add or remove LiteLLM-only model configurations. - ---- - -## Step 8: Set Up SSL Certificates (Development Only) - -**WARNING: THIS IS FOR DEV ONLY** -When deploying for dev and testing you can use a self-signed certificate for the REST API ALB. You can create this by using the script: `gen-cert.sh` and uploading it to `IAM`. - -```bash -export REGION= -./scripts/gen-certs.sh -aws iam upload-server-certificate --server-certificate-name --certificate-body file://scripts/server.pem --private-key file://scripts/server.key -``` - -Update your `config.yaml` with the certificate ARN: - -```yaml -restApiConfig: - loadBalancerConfig: - sslCertIamArn: arn:aws:iam:::server-certificate/ -``` ---- - -## Step 9: Customize Model Deployment - -In the `ecsModels` section of `config.yaml`, allow our deployment process to pull the model weights for you. - -During the deployment process, LISA will optionally attempt to download your model weights if you specify an optional `ecsModels` -array, this will only work in non ADC regions. Specifically, see the `ecsModels` section of the [example_config.yaml](./example_config.yaml) file. -Here we define the model name, inference container, and baseImage: - -```yaml -ecsModels: - - modelName: your-model-name - inferenceContainer: tgi - baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 -``` - ---- - -## Step 10: Bootstrap CDK (If Not Already Done) - -If you haven't bootstrapped your AWS account for CDK: - -```bash -make bootstrap -``` - ---- - -## Recommended LiteLLM Configuration Options - -While LISA is designed to be flexible, configuring external models requires careful consideration. The following guide -provides a recommended minimal setup for integrating various model types with LISA using LiteLLM. - -### Configuration Overview - -This example configuration demonstrates how to set up: -1. A SageMaker Endpoint -2. An Amazon Bedrock Model -3. A self-hosted OpenAI-compatible text generation model -4. A self-hosted OpenAI-compatible embedding model - -**Note:** Ensure that all endpoints and models are in the same AWS region as your LISA installation. - -### SageMaker Endpoints and Bedrock Models - -LISA supports adding existing SageMaker Endpoints and Bedrock Models to the LiteLLM configuration. As long as these -services are in the same region as the LISA installation, LISA can use them alongside any other deployed models. - -**To use a SageMaker Endpoint:** -1. Install LISA without initially referencing the SageMaker Endpoint. -2. Create a SageMaker Model using the private subnets of the LISA deployment. -3. This setup allows the LISA REST API container to communicate with any Endpoint using that SageMaker Model. - -**SageMaker Endpoints and Bedrock Models can be configured:** -- Statically at LISA deployment time -- Dynamically using the LISA Model Management API - -**Important:** Endpoints or Models statically defined during LISA deployment cannot be removed or updated using the -LISA Model Management API, and they will not show in the Chat UI. These will only show as part of the OpenAI `/models` API. -Although there is support for it, we recommend using the [Model Management API](#admin-level-model-management-api) instead of the following static configuration. - -### Example Configuration - -```yaml -dev: - litellmConfig: - litellm_settings: - telemetry: false # Disable telemetry to LiteLLM servers (recommended for VPC deployments) - drop_params: true # Ignore unrecognized parameters instead of failing - - model_list: - # 1. SageMaker Endpoint Configuration - - model_name: test-endpoint # Human-readable name, can be anything and will be used for OpenAI API calls - litellm_params: - model: sagemaker/test-endpoint # Prefix required for SageMaker Endpoints and "test-endpoint" matches Endpoint name - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: textgen - streaming: true - - # 2. Amazon Bedrock Model Configuration - - model_name: bedrock-titan-express # Human-readable name for future OpenAI API calls - litellm_params: - model: bedrock/amazon.titan-text-express-v1 # Prefix required for Bedrock Models, and exact name of Model to use - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: textgen - streaming: true - - # 3. Custom OpenAI-compatible Text Generation Model - - model_name: custom-openai-model # Used in future OpenAI-compatible calls to LiteLLM - litellm_params: - model: openai/custom-provider/textgen-model # Format: openai// - api_base: https://your-domain-here:443/v1 # Your model's base URI - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: textgen - streaming: true - - # 4. Custom OpenAI-compatible Embedding Model - - model_name: custom-openai-embedding-model # Used in future OpenAI-compatible calls to LiteLLM - litellm_params: - model: openai/modelProvider/modelName # Prefix required for OpenAI-compatible models followed by model provider and name details - api_base: https://your-domain-here:443/v1 # Your model's base URI - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: embedding -``` - ---- - -# API Usage Overview - -LISA provides robust API endpoints for managing models, both for users and administrators. These endpoints allow for operations such as listing, creating, updating, and deleting models. - -## API Gateway and ALB Endpoints - -LISA uses two primary APIs for model management: - -1. **User-facing OpenAI-Compatible API**: Available to all users for inference tasks and accessible through the LISA Serve ALB. This API provides an interface for querying and interacting with models deployed on Amazon ECS, Amazon Bedrock, or through LiteLLM. -2. **Admin-level Model Management API**: Available only to administrators through the API Gateway (APIGW). This API allows for full control of model lifecycle management, including creating, updating, and deleting models. - -### LiteLLM Routing in All Models - -Every model request is routed through LiteLLM, regardless of whether infrastructure (like ECS) is created for it. Whether deployed on ECS, external models via Bedrock, or managed through LiteLLM, all models are added to LiteLLM for traffic routing. The distinction is whether infrastructure is created (determined by request payloads), but LiteLLM integration is consistent for all models. The model management APIs will handle adding or removing model configurations from LiteLLM, and the LISA Serve endpoint will handle the inference requests against models available in LiteLLM. - -## User-facing OpenAI-Compatible API - -The OpenAI-compatible API is accessible through the LISA Serve ALB and allows users to list models available for inference tasks. Although not specifically part of the model management APIs, any model that is added or removed from LiteLLM via the model management API Gateway APIs will be reflected immediately upon queries to LiteLLM through the LISA Serve ALB. - -### Listing Models - -The `/v2/serve/models` endpoint on the LISA Serve ALB allows users to list all models available for inference in the LISA system. - -#### Request Example: - -```bash -curl -s -H 'Authorization: Bearer ' -X GET https:///v2/serve/models -``` - -#### Response Example: - -```json -{ - "data": [ - { - "id": "bedrock-embed-text-v2", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - }, - { - "id": "titan-express-v1", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - }, - { - "id": "sagemaker-amazon-mistrallite", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - } - ], - "object": "list" -} -``` - -#### Explanation of Response Fields: - -These fields are all defined by the OpenAI API specification, which is documented [here](https://platform.openai.com/docs/api-reference/models/list). - -- `id`: A unique identifier for the model. -- `object`: The type of object, which is "model" in this case. -- `created`: A Unix timestamp representing when the model was created. -- `owned_by`: The entity responsible for the model, such as "openai." - -## Admin-level Model Management API - -This API is only accessible by administrators via the API Gateway and is used to create, update, and delete models. It supports full model lifecycle management. - -### Listing Models (Admin API) - -The `/models` route allows admins to list all models managed by the system. This includes models that are either creating, deleting, already active, or in a failed state. Models can be deployed via ECS or managed externally through a LiteLLM configuration. - -#### Request Example: - -```bash -curl -s -H "Authorization: Bearer " -X GET https:///models -``` - -#### Response Example: - -```json -{ - "models": [ - { - "autoScalingConfig": { - "minCapacity": 1, - "maxCapacity": 1, - "cooldown": 420, - "defaultInstanceWarmup": 180, - "metricConfig": { - "albMetricName": "RequestCountPerTarget", - "targetValue": 30, - "duration": 60, - "estimatedInstanceWarmup": 330 - } - }, - "containerConfig": { - "image": { - "baseImage": "vllm/vllm-openai:v0.5.0", - "type": "asset" - }, - "sharedMemorySize": 2048, - "healthCheckConfig": { - "command": [ - "CMD-SHELL", - "exit 0" - ], - "interval": 10, - "startPeriod": 30, - "timeout": 5, - "retries": 3 - }, - "environment": { - "MAX_TOTAL_TOKENS": "2048", - "MAX_CONCURRENT_REQUESTS": "128", - "MAX_INPUT_LENGTH": "1024" - } - }, - "loadBalancerConfig": { - "healthCheckConfig": { - "path": "/health", - "interval": 60, - "timeout": 30, - "healthyThresholdCount": 2, - "unhealthyThresholdCount": 10 - } - }, - "instanceType": "g5.xlarge", - "modelId": "mistral-vllm", - "modelName": "mistralai/Mistral-7B-Instruct-v0.2", - "modelType": "textgen", - "modelUrl": null, - "status": "Creating", - "streaming": true - }, - { - "autoScalingConfig": null, - "containerConfig": null, - "loadBalancerConfig": null, - "instanceType": null, - "modelId": "titan-express-v1", - "modelName": "bedrock/amazon.titan-text-express-v1", - "modelType": "textgen", - "modelUrl": null, - "status": "InService", - "streaming": true - } - ] -} -``` - -#### Explanation of Response Fields: - -- `modelId`: A unique identifier for the model. -- `modelName`: The name of the model, typically referencing the underlying service (Bedrock, SageMaker, etc.). -- `status`: The current state of the model, e.g., "Creating," "Active," or "Failed." -- `streaming`: Whether the model supports streaming inference. -- `instanceType` (optional): The instance type if the model is deployed via ECS. - -### Creating a Model (Admin API) - -LISA provides the `/models` endpoint for creating both ECS and LiteLLM-hosted models. Depending on the request payload, infrastructure will be created or bypassed (e.g., for LiteLLM-only models). - -This API accepts the same model definition parameters that were accepted in the V2 model definitions within the config.yaml file with one notable difference: the `containerConfig.image.path` field is -now omitted because it corresponded with the `inferenceContainer` selection. As a convenience, this path is no longer required. - -#### Request Example: - -``` -POST https:///models -``` - -#### Example Payload for ECS Model: - -```json -{ - "modelId": "mistral-vllm", - "modelName": "mistralai/Mistral-7B-Instruct-v0.2", - "modelType": "textgen", - "inferenceContainer": "vllm", - "instanceType": "g5.xlarge", - "streaming": true, - "containerConfig": { - "image": { - "baseImage": "vllm/vllm-openai:v0.5.0", - "type": "asset" - }, - "sharedMemorySize": 2048, - "environment": { - "MAX_CONCURRENT_REQUESTS": "128", - "MAX_INPUT_LENGTH": "1024", - "MAX_TOTAL_TOKENS": "2048" - }, - "healthCheckConfig": { - "command": ["CMD-SHELL", "exit 0"], - "interval": 10, - "startPeriod": 30, - "timeout": 5, - "retries": 3 - } - }, - "autoScalingConfig": { - "minCapacity": 1, - "maxCapacity": 1, - "cooldown": 420, - "defaultInstanceWarmup": 180, - "metricConfig": { - "albMetricName": "RequestCountPerTarget", - "targetValue": 30, - "duration": 60, - "estimatedInstanceWarmup": 330 - } - }, - "loadBalancerConfig": { - "healthCheckConfig": { - "path": "/health", - "interval": 60, - "timeout": 30, - "healthyThresholdCount": 2, - "unhealthyThresholdCount": 10 - } - } -} -``` - -#### Creating a LiteLLM-Only Model: - -```json -{ - "modelId": "titan-express-v1", - "modelName": "bedrock/amazon.titan-text-express-v1", - "modelType": "textgen", - "streaming": true -} -``` - -#### Explanation of Key Fields for Creation Payload: - -- `modelId`: The unique identifier for the model. This is any name you would like it to be. -- `modelName`: The name of the model as it appears in the system. For LISA-hosted models, this must be the S3 Key to your model artifacts, otherwise - this is the LiteLLM-compatible reference to a SageMaker Endpoint or Bedrock Foundation Model. Note: Bedrock and SageMaker resources must exist in the - same region as your LISA deployment. If your LISA installation is in us-east-1, then all SageMaker and Bedrock calls will also happen in us-east-1. - Configuration examples: - - LISA hosting: If your model artifacts are in `s3://${lisa_models_bucket}/path/to/model/weights`, then the `modelName` value here should be `path/to/model/weights` - - LiteLLM-only, Bedrock: If you want to use `amazon.titan-text-lite-v1`, your `modelName` value should be `bedrock/amazon.titan-text-lite-v1` - - LiteLLM-only, SageMaker: If you want to use a SageMaker Endpoint named `my-sm-endpoint`, then the `modelName` value should be `sagemaker/my-sm-endpoint`. -- `modelType`: The type of model, such as text generation (textgen). -- `streaming`: Whether the model supports streaming inference. -- `instanceType`: The type of EC2 instance to be used (only applicable for ECS models). -- `containerConfig`: Details about the Docker container, memory allocation, and environment variables. -- `autoScalingConfig`: Configuration related to ECS autoscaling. -- `loadBalancerConfig`: Health check configuration for load balancers. - -### Deleting a Model (Admin API) - -Admins can delete a model using the following endpoint. Deleting a model removes the infrastructure (ECS) or disconnects from LiteLLM. - -#### Request Example: - -``` -DELETE https:///models/{modelId} -``` - -#### Response Example: - -```json -{ - "status": "success", - "message": "Model mistral-vllm has been deleted successfully." -} -``` - -### Updating a Model - -LISA offers basic updating functionality for both LISA-hosted and LiteLLM-only models. For both types, the model type and streaming support can be updated -in the cases that the models were originally created with the wrong parameters. For example, if an embedding model was accidentally created as a `textgen` -model, the UpdateModel API can be used to set it to the intended `embedding` value. Additionally, for LISA-hosted models, users may update the AutoScaling -configuration to increase or decrease capacity usage for each model. Users may use this API to completely shut down all instances behind a model until -they want to add capacity back to the model for usage later. This feature can help users to effectively manage costs so that instances do not have to stay -running in time periods of little or no expected usage. - -The UpdateModel API has mutually exclusive payload fields to avoid conflicting requests. The API does not allow for shutting off a model at the same time -as updating its AutoScaling configuration, as these would introduce ambiguous intents. The API does not allow for setting AutoScaling limits to 0 and instead -requires the usage of the enable/disable functionality to allow models to fully scale down or turn back on. Metadata updates, such as changing the model type -or streaming compatibility, can happen in either type of update or simply by themselves. - -#### Request Example - -``` -PUT https:///models/{modelId} -``` - -#### Example Payloads - -##### Update Model Metadata - -This payload will simply update the model metadata, which will complete within seconds of invoking. If setting a model as an `embedding` model, then the -`streaming` option must be set to `false` or omitted as LISA does not support streaming with embedding models. Both the `streaming` and `modelType` options -may be included in any other update request. - -```json -{ - "streaming": true, - "modelType": "textgen" -} -``` - -##### Update AutoScaling Configuration - -This payload will update the AutoScaling configuration for minimum, maximum, and desired number of instances. The desired number must be between the -minimum or maximum numbers, inclusive, and all the numbers must be strictly greater than 0. If the model currently has less than the minimum number, then -the desired count will automatically raise to the minimum if a desired count is not specified. Despite setting a desired capacity, the model will scale down -to the minimum number over time if you are not hitting the scaling thresholds set when creating the model in the first place. - -The AutoScaling configuration **can** be updated while the model is in the Stopped state, but it won't be applied immediately. Instead, the configuration will -be saved until the model is started again, in which it will use the most recently updated AutoScaling configuration. - -The request will fail if the `autoScalingInstanceConfig` is defined at the same time as the `enabled` field. These options are mutually exclusive and must be -handled as separate operations. Any or all of the options within the `autoScalingInstanceConfig` may be set as needed, so if you only wish to change the `desiredCapacity`, -then that is the only option that you need to specify in the request object within the `autoScalingInstanceConfig`. - -```json -{ - "autoScalingInstanceConfig": { - "minCapacity": 2, - "maxCapacity": 4, - "desiredCapacity": 3 - } -} -``` - -##### Stop Model - Scale Down to 0 Instances - -This payload will stop all model EC2 instances and remove the model reference from LiteLLM so that users are unable to make inference requests against a model -with no capacity. This option is useful for users who wish to manage costs and turn off instances when the model is not currently needed but will be used again -in the future. - -The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be -handled as separate operations. - -```json -{ - "enabled": false -} -``` - -##### Start Model - Restore Previous AutoScaling Configuration - -After stopping a model, this payload will turn the model back on by spinning up instances, waiting for the expected spin-up time to allow models to initialize, and then -adding the reference back to LiteLLM so that users may query the model again. This is expected to be a much faster operation than creating the model through the CreateModel -API, so as long as the model details don't have to change, this in combination with the Stop payload will help to manage costs while still providing model availability as -quickly as the system can spin it up again. - -The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be -handled as separate operations. - -```json -{ - "enabled": true -} -``` - ---- - -# Error Handling for API Requests - -In the LISA model management API, error handling is designed to ensure robustness and consistent responses when errors occur during the execution of API requests. This section provides a detailed explanation of the error handling mechanisms in place, including the types of errors that are managed, how they are raised, and what kind of responses clients can expect when these errors occur. - -## Common Errors and Their HTTP Responses - -Below is a list of common errors that can occur in the system, along with the HTTP status codes and response structures that are returned to the client. - -### ModelNotFoundError - -* **Description**: Raised when a model that is requested for retrieval or deletion is not found in the system. -* **HTTP Status Code**: `404 Not Found` -* **Response Body**: - -```json -{ - "error": "ModelNotFoundError", - "message": "The requested model with ID could not be found." -} -``` - -* **Example Scenario**: When a client attempts to fetch details of a model that does not exist in the database, the `ModelNotFoundError` is raised. - -### ModelAlreadyExistsError - -* **Description:** Raised when a request to create a model is made, but the model already exists in the system. -* **HTTP Status Code**: `400` -* **Response Body**: - -```json -{ - "error": "ModelAlreadyExistsError", - "message": "A model with the given configuration already exists." -} -``` - -* **Example Scenario:** A client attempts to create a model with an ID or name that already exists in the database. The system detects the conflict and raises the `ModelAlreadyExistsError`. - -### InvalidInputError (Hypothetical Example) - -* **Description**: Raised when the input provided by the client for creating or updating a model is invalid or does not conform to expected formats. -* **HTTP Status Code**: `400 Bad Request` -* **Response Body**: - -```json -{ - "error": "InvalidInputError", - "message": "The input provided is invalid. Please check the required fields and formats." -} -``` - -* **Example Scenario**: The client submits a malformed JSON body or omits required fields in a model creation request, triggering an `InvalidInputError`. - -## Handling Validation Errors - -Validation errors are handled across the API via utility functions and model transformation logic. These errors typically occur when user inputs fail validation checks or when required data is missing from a request. - -### Example Response for Validation Error: - -* **HTTP Status Code**: `422 Unprocessable Entity` -* **Response Body**: - -```json -{ - "error": "ValidationError", - "message": "The input provided does not meet the required validation criteria." -} -``` - ---- - -# Deployment -## Using pre-built resources - -A default configuration will build the necessary containers, lambda layers, and production optimized -web application at build time. In the event that you would like to use pre-built resources due to -network connectivity reasons or other concerns with the environment where you'll be deploying LISA -you can do so. - -- For ECS containers (Models, APIs, etc) you can modify the `containerConfig` block of - the corresponding entry in `config.yaml`. For container images you can provide a path to a directory - from which a docker container will be built (default), a path to a tarball, an ECR repository arn and - optional tag, or a public registry path. - - We provide immediate support for HuggingFace TGI and TEI containers and for vLLM containers. The `example_config.yaml` - file provides examples for TGI and TEI, and the only difference for using vLLM is to change the - `inferenceContainer`, `baseImage`, and `path` options, as indicated in the snippet below. All other options can - remain the same as the model definition examples we have for the TGI or TEI models. vLLM can also support embedding - models in this way, so all you need to do is refer to the embedding model artifacts and remove the `streaming` field - to deploy the embedding model. - - vLLM has support for the OpenAI Embeddings API, but model support for it is limited because the feature is new. Currently, - the only supported embedding model with vLLM is [intfloat/e5-mistral-7b-instruct](https://huggingface.co/intfloat/e5-mistral-7b-instruct), - but this list is expected to grow over time as vLLM updates. - ```yaml - ecsModels: - - modelName: your-model-name - inferenceContainer: tgi - baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 - ``` -- If you are deploying the LISA Chat User Interface you can optionally specify the path to the pre-built - website assets using the top level `webAppAssetsPath` parameter in `config.yaml`. Specifying this path - (typically `lib/user-interface/react/dist`) will avoid using a container to build and bundle the assets - at CDK build time. -- For the lambda layers you can specify the path to a local zip archive of the layer code by including - the optional `lambdaLayerAssets` block in `config.yaml` similar to the following: - -``` -lambdaLayerAssets: - authorizerLayerPath: lib/core/layers/authorizer_layer.zip - commonLayerPath: lib/core/layers/common_layer.zip - fastapiLayerPath: /path/to/fastapi_layer.zip - sdkLayerPath: lib/rag/layers/sdk_layer.zip -``` ---- - -## Deploying - -Now that we have everything setup we are ready to deploy. - -```bash -make deploy -``` - -By default, all stacks will be deployed but a particular stack can be deployed by providing the `STACK` argument to the `deploy` target. - -```bash -make deploy STACK=LisaServe -``` - -Available stacks can be listed by running: - -```bash -make listStacks -``` - -After the `deploy` command is run, you should see many docker build outputs and eventually a CDK progress bar. The deployment should take about 10-15 minutes and will produce a single cloud formation output for the websocket URL. - -You can test the deployment with the integration test: - -```bash -pytest lisa-sdk/tests --url --verify | false -``` - ---- - -## Programmatic API Tokens - -The LISA Serve ALB can be used for programmatic access outside the example Chat application. -An example use case would be for allowing LISA to serve LLM requests that originate from the [Continue VSCode Plugin](https://www.continue.dev/). -To facilitate communication directly with the LISA Serve ALB, a user with sufficient DynamoDB PutItem permissions may add -API keys to the APITokenTable, and once created, a user may make requests by including the `Authorization: Bearer ${token}` -header or the `Api-Key: ${token}` header with that token. If using any OpenAI-compatible library, the `api_key` fields -will use the `Authorization: Bearer ${token}` format automatically, so there is no need to include additional headers -when using those libraries. - -### Adding a Token - -An account owner may create a long-lived API Token using the following AWS CLI command. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" # change to a unique string for a user -aws --region $AWS_REGION dynamodb put-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --item '{"token": {"S": "'${token_string}'"}}' -``` - -If an account owner wants the API Token to be temporary and expire after a specific date, LISA will allow for this too. -In addition to the `token` field, the owner may specify the `tokenExpiration` field, which accepts a UNIX timestamp, -in seconds. The following command shows an example of how to do this. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" -token_expiration=$(echo $(date +%s) + 3600 | bc) # token that expires in one hour, 3600 seconds -aws --region $AWS_REGION dynamodb put-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --item '{ - "token": {"S": "'${token_string}'"}, - "tokenExpiration": {"N": "'${token_expiration}'"} - }' -``` - -Once the token is inserted into the DynamoDB Table, a user may use the token in the `Authorization` request header like -in the following snippet. - -```bash -lisa_serve_rest_url="https://" -token_string="YOUR_STRING_HERE" -curl ${lisa_serve_rest_url}/v2/serve/models \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -H "Authorization: Bearer ${token_string}" -``` - -### Updating a Token - -In the case that an owner wishes to change an existing expiration time or add one to a key that did not previously have -an expiration, this can be accomplished by editing the existing item. The following commands can be used as an example -for updating an existing token. Setting the expiration time to a time in the past will effectively remove access for -that key. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" -token_expiration=$(echo $(date +%s) + 600 | bc) # token that expires in 10 minutes from now -aws --region $AWS_REGION dynamodb update-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --key '{"token": {"S": "'${token_string}'"}}' \ - --update-expression 'SET tokenExpiration=:t' \ - --expression-attribute-values '{":t": {"N": "'${token_expiration}'"}}' -``` - -### Removing a Token - -Tokens will not be automatically removed even if they are no longer valid. An owner may remove an key, expired or not, -from the database to fully revoke the key, by deleting the item. As an example, the following commands can be used to -remove a token. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" # change to the token to remove -aws --region $AWS_REGION dynamodb delete-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --key '{"token": {"S": "'${token_string}'"}}' -``` - ---- - -## Model Compatibility - -### HuggingFace Generation Models - -For generation models, or causal language models, LISA supports models that are supported by the underlying serving container, TGI. TGI divides compatibility into two categories: optimized models and best effort supported models. The list of optimized models is found [here](https://huggingface.co/docs/text-generation-inference/supported_models). The best effort uses the `transformers` codebase under-the-hood and so should work for most causal models on HuggingFace: - -```python -AutoModelForCausalLM.from_pretrained(, device_map="auto") -``` - -or - -```python -AutoModelForSeq2SeqLM.from_pretrained(, device_map="auto") -``` - -### HuggingFace Embedding Models - -Embedding models often utilize custom codebases and are not as uniform as generation models. For this reason you will likely need to create a new `inferenceContainer`. Follow the [example](./lib/ecs-model/embedding/instructor) provided for the `instructor` model. - -### vLLM Models - -In addition to the support we have for the TGI and TEI containers, we support hosting models using the [vLLM container](https://docs.vllm.ai/en/latest/). vLLM abides by the OpenAI specification, and as such allows both text generation and embedding on the models that vLLM supports. -See the [deployment](#deployment) section for details on how to set up the vLLM container for your models. Similar to how the HuggingFace containers will serve safetensor weights downloaded from the -HuggingFace website, vLLM will do the same, and our configuration will allow you to serve these artifacts automatically. vLLM does not have many supported models for embeddings, but as they become available, -LISA will support them as long as the vLLM container version is updated in the config.yaml file and as long as the model's safetensors can be found in S3. - ---- - -# Chatbot Example - -This repository include an example chatbot web application. The react based web application can be optionally deployed to demonstrate the capabilities of LISA Serve. The chatbot consists of a static react based single page application hosted via API GW S3 proxy integration. The app connects to the LISA Serve REST API and an optional RAG API. The app integrates with an OIDC compatible IdP and allows users to interact directly with any of the textgen models hosted with LISA Serve. If the optional RAG stack is deployed then users can also leverage the embeddings models and AWS OpenSearch or PGVector to demonstrate chat with RAG. Chat sessions are maintained in dynamodb table and a number of parameters are exposed through the UI to allow experimentation with various parameters including prompt, temperature, top k, top p, max tokens, and more. - -## Local development - -### Configuring Pre-Commit Hooks - -To ensure code quality and consistency, this project uses pre-commit hooks. These hooks are configured to perform checks, such as linting and formatting, helping to catch potential issues early. These hooks are run automatically on each push to a remote branch but if you wish to run them locally before each commit, follow these steps: - -1. Install pre-commit: `pip install pre-commit` -2. Install the git hook scripts: `pre-commit install` - -The hooks will now run automatically on changed files but if you wish to test them against all files, run the following command: `pre-commit run --all-files`. - -### Run REST API locally - -``` -cd lib/serve/rest-api -pip install -r src/requirements.txt -export AWS_REGION= -export AUTHORITY= -export CLIENT_ID= -export REGISTERED_MODELS_PS_NAME= -export TOKEN_TABLE_NAME="/LISAApiTokenTable" -gunicorn -k uvicorn.workers.UvicornWorker -w 2 -b "0.0.0.0:8080" "src.main:app" -``` - -### Run example chatbot locally - -Create `lib/user-interface/react/public/env.js` file with the following contents: - -``` -window.env = { - AUTHORITY: '', - CLIENT_ID: '', - JWT_GROUPS_PROP: '', - ADMIN_GROUP: '', - CUSTOM_SCOPES:[], - // Alternatively you can set this to be your REST api elb endpoint - RESTAPI_URI: 'http://localhost:8080/', - API_BASE_URL: 'https://${deployment_id}.execute-api.${regional_domain}/${deployment_stage}', - RESTAPI_VERSION: 'v2', - "MODELS": [ - { - "model": "streaming-textgen-model", - "streaming": true, - "modelType": "textgen" - }, - { - "model": "non-streaming-textgen-model", - "streaming": false, - "modelType": "textgen" - }, - { - "model": "embedding-model", - "streaming": null, - "modelType": "embedding" - } - ] -} -``` - -Launch the Chat UI: - -``` -cd lib/user-interface/react/ -npm run dev -``` ---- - -# Usage and Features - -The LISA Serve endpoint can be used independently of the Chat UI, and the following shows a few examples of how to do that. The Serve endpoint -will still validate user auth, so if you have a Bearer token from the IdP configured with LISA, we will honor it, or if you've set up an API -token using the [DynamoDB instructions](#programmatic-api-tokens), we will also accept that. This diagram shows the LISA Serve components that -would be utilized during direct REST API requests. - -## OpenAI Specification Compatibility - -We now provide greater support for the [OpenAI specification](https://platform.openai.com/docs/api-reference) for model inference and embeddings. -We utilize LiteLLM as a proxy for both models we spin up on behalf of the user and additional models configured through the config.yaml file, and because of that, the -LISA REST API endpoint allows for a central location for making text generation and embeddings requests. We support, and are not limited to, the following popular endpoint -routes as long as your underlying models can also respond to them. - -- /models -- /chat/completions -- /completions -- /embeddings - -By supporting the OpenAI spec, we can more easily allow users to integrate their collection of models into their LLM applications and workflows. In LISA, users can authenticate -using their OpenID Connect Identity Provider, or with an API token created through the DynamoDB token workflow as described [here](#programmatic-api-tokens). Once the token -is retrieved, users can use that in direct requests to the LISA Serve REST API. If using the IdP, users must set the 'Authorization' header, otherwise if using the API token, -either the 'Api-Key' header or the 'Authorization' header. After that, requests to `https://${lisa_serve_alb}/v2/serve` will handle the OpenAI API calls. As an example, the following call can list all -models that LISA is aware of, assuming usage of the API token. If you are using a self-signed cert, you must also provide the `--cacert $path` option to specify a CA bundle to trust for SSL verification. - -```shell -curl -s -H 'Api-Key: your-token' -X GET https://${lisa_serve_alb}/v2/serve/models -``` - -If using the IdP, the request would look like the following: - -```shell -curl -s -H 'Authorization: Bearer your-token' -X GET https://${lisa_serve_alb}/v2/serve/models -``` - -When using a library that requests an OpenAI-compatible base_url, you can provide `https://${lisa_serve_alb}/v2/serve` here. All of the OpenAI routes will -automatically be added to the base URL, just as we appended `/models` to the `/v2/serve` route for listing all models tracked by LISA. - ---- - -## Continue JetBrains and VS Code Plugin - -For developers that desire an LLM assistant to help with programming tasks, we support adding LISA as an LLM provider for the [Continue plugin](https://www.continue.dev). -To add LISA as a provider, open up the Continue plugin's `config.json` file and locate the `models` list. In this list, add the following block, replacing the placeholder URL -with your own REST API domain or ALB. The `/v2/serve` is required at the end of the `apiBase`. This configuration requires an API token as created through the [DynamoDB workflow](#programmatic-api-tokens). - -```json -{ - "model": "AUTODETECT", - "title": "LISA", - "apiBase": "https:///v2/serve", - "provider": "openai", - "apiKey": "your-api-token" // pragma: allowlist-secret -} -``` - -Once you save the `config.json` file, the Continue plugin will call the `/models` API to get a list of models at your disposal. The ones provided by LISA will be prefaced -with "LISA" or with the string you place in the `title` field of the config above. Once the configuration is complete and a model is selected, you can use that model to -generate code and perform AI assistant tasks within your development environment. See the [Continue documentation](https://docs.continue.dev/how-to-use-continue) for more -information about its features, capabilities, and usage. - -### Usage in LLM Libraries - -If your workflow includes using libraries, such as [LangChain](https://python.langchain.com/v0.2/docs/introduction/) or [OpenAI](https://github.com/openai/openai-python), -then you can place LISA right in your application by changing only the endpoint and headers for the client objects. As an example, using the OpenAI library, the client would -normally be instantiated and invoked with the following block. - -```python -from openai import OpenAI - -client = OpenAI( - api_key="my_key" # pragma: allowlist-secret not a real key -) -client.models.list() -``` - -To use the models being served by LISA, the client needs only a few changes: - -1. Specify the `base_url` as the LISA Serve ALB, using the /v2/serve route at the end, similar to the apiBase in the [Continue example](#continue-jetbrains-and-vs-code-plugin) -2. Add the API key that you generated from the [token generation steps](#programmatic-api-tokens) as your `api_key` field. -3. If using a self-signed cert, you must provide a certificate path for validating SSL. If you're using an ACM or public cert, then this may be omitted. -1. We provide a convenience function in the `lisa-sdk` for generating a cert path from an IAM certificate ARN if one is provided in the `RESTAPI_SSL_CERT_ARN` environment variable. - -The Code block will now look like this and you can continue to use the library without any other modifications. - -```python -# for self-signed certificates -import boto3 -from lisapy.utils import get_cert_path -# main client library -from openai import DefaultHttpxClient, OpenAI - -iam_client = boto3.client("iam") -cert_path = get_cert_path(iam_client) - -client = OpenAI( - api_key="my_key", # pragma: allowlist-secret not a real key - base_url="https:///v2/serve", - http_client=DefaultHttpxClient(verify=cert_path), # needed for self-signed certs on your ALB, can be omitted otherwise -) -client.models.list() -``` - ---- - -# License Notice -Although this repository is released under the Apache 2.0 license, when configured to use PGVector as a RAG store it uses -the third party `psycopg2-binary` library. The `psycopg2-binary` project's licensing includes the [LGPL with exceptions](https://github.com/psycopg/psycopg2/blob/master/LICENSE) license. +## What is LISA? + +LISA is an infrastructure-as-code solution providing scalable, low latency access to customers’ generative LLMs and +embedding language models. LISA accelerates and supports customers’ GenAI experimentation and adoption, particularly in +regions where Amazon Bedrock is not available. LISA allows customers to move quickly rather than independently solve the +undifferentiated heavy lifting of hosting and inference architecture. Customers deploy LISA into a single AWS account +and integrate it with an identity provider. Customers bring their own models to LISA for self-hosting and inference +supported by Amazon Elastic Container Service (ECS). Model configuration is managed through LISA’s model management +APIs. + +As use cases and model requirements grow, customers can configure LISA with external model providers. Through OpenAI's +API spec via the LiteLLM proxy, LISA is compatible with 100+ models from various providers, including Amazon Bedrock and +Amazon Jumpstart. LISA customers can centralize communication across many model providers via LiteLLM, leveraging LISA +for model orchestration. Using LISA as a model orchestration layer allows customers to standardize integrations with +externally hosted models in a single place. Without an orchestration layer, customers must individually manage unique +API integrations with each provider. + +## Key Features + +* Self Host Models: Bring your own text generation and embedding models to LISA for hosting and inference. +* Model Orchestration: Centralize and standardize configuration with 100+ models from model providers via LiteLLM, + including Amazon Bedrock models. +* Chatbot User Interface: Through the chatbot user interface, users can prompt LLMs, receive responses, modify prompt + templates, change model arguments, and manage their session history. Administrators can control available features via + the configuration page. +* Retrieval-augmented generation (RAG): RAG reduces the need for fine-tuning, an expensive and time-consuming + undertaking, and delivers more contextually relevant outputs. LISA offers RAG through Amazon OpenSearch or + PostgreSQL’s PGVector extension on Amazon RDS. +* Non-RAG Model Context: Users can upload documents to their chat sessions to enhance responses or support use cases + like document summarization. +* Model Management: Administrators can add, remove, and update models configured with LISA through the model management + configuration page or APIs. +* OpenAI API spec: LISA can be configured with compatible tooling. For example, customers can configure LISA as the + model provider for the Continue plugin, an open-source AI code assistance for JetBrains and Visual Studio Code + integrated development environments (IDEs). This allows users to select from any LISA-configured model to support LLM + prompting directly in their IDE. +* Libraries: If your workflow includes libraries such as [LangChain](https://python.langchain.com/) + or [OpenAI](https://github.com/openai/openai-python), then you can place LISA in your + application by changing only the endpoint and headers for the client objects. +* FedRAMP: The AWS services that LISA leverages are FedRAMP High compliant. +* Ongoing Releases: We offer on-going release with new functionality. LISA’s roadmap is customer driven. + +## Deployment Prerequisites + +### Pre-Deployment Steps + +* Set up and have access to an AWS account with appropriate permissions + * All the resource creation that happens as part of CDK deployments expects Administrator or Administrator-like + permissions with resource creation and mutation permissions. Installation will not succeed if this profile does + not have permissions to create and edit arbitrary resources for the system. Note: This level of permissions is not + required for the runtime of LISA. This is only necessary for deployment and subsequent updates. +* Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles +* Optional: If using the chat UI, Have your Identity Provider (IdP) information and access +* Optional: Have your VPC information available, if you are using an existing one for your deployment +* Note: CDK briefly leverages SSM. Confirm it is approved for use by your organization before beginning. + +### Software + +* AWS CLI installed and configured +* Python 3.9 or later +* Node.js 14 or later +* Docker installed and running +* Sufficient disk space for model downloads and conversions + + +## Getting Started + +For detailed instructions on setting up, configuring, and deploying LISA, please refer to our separate documentation on +installation and usage. + +[![LISA Documentation](https://img.shields.io/badge/LISA%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/LISA/) + +## License + +Although this repository is released under the Apache 2.0 license, when configured to use PGVector as a RAG store it +uses +the third party `psycopg2-binary` library. The `psycopg2-binary` project's licensing includes +the [LGPL with exceptions](https://github.com/psycopg/psycopg2/blob/master/LICENSE) license. diff --git a/ecs_model_deployer/src/lib/ecs-model.ts b/ecs_model_deployer/src/lib/ecs-model.ts index 66016cf6..1deb6a7a 100644 --- a/ecs_model_deployer/src/lib/ecs-model.ts +++ b/ecs_model_deployer/src/lib/ecs-model.ts @@ -12,7 +12,7 @@ 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. -*/ + */ // ECS Model Construct. import { ISecurityGroup, IVpc, SubnetSelection } from 'aws-cdk-lib/aws-ec2'; diff --git a/lib/docs/.gitignore b/lib/docs/.gitignore index 80ac0588..99d9f534 100644 --- a/lib/docs/.gitignore +++ b/lib/docs/.gitignore @@ -1,2 +1,3 @@ dist/ .vitepress/cache/ +/config/schema.md diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts index df61b006..ce20165a 100644 --- a/lib/docs/.vitepress/config.mts +++ b/lib/docs/.vitepress/config.mts @@ -20,29 +20,23 @@ const navLinks = [ { text: 'System Administrator Guide', items: [ + { text: 'What is Lisa?', link: '/admin/overview' }, { text: 'Architecture Overview', link: '/admin/architecture' }, - { text: 'LISA Components', link: '/admin/components' }, { text: 'Getting Started', link: '/admin/getting-started' }, - { text: 'Configure IdP: Cognito & Keycloak Examples', link: '/admin/idp' }, { text: 'Deployment', link: '/admin/deploy' }, - { text: 'Setting Model Management Admin Group', link: '/admin/model-management' }, - { text: 'LiteLLM', link: '/admin/lite-llm' }, - { text: 'API Overview', link: '/admin/api' }, + { text: 'Model Management API Usage', link: '/admin/model-management' }, { text: 'API Request Error Handling', link: '/admin/error' }, - { text: 'Security', link: '/admin/security' }, ], }, { text: 'Advanced Configuration', items: [ - { text: 'Programmatic API Tokens', link: '/config/api-tokens' }, + { text: 'Configuration Schema', link: '/config/configuration' }, { text: 'Model Compatibility', link: '/config/model-compatibility' }, - { text: 'Model Management API', link: '/config/model-management-api' }, - { text: 'Model Management UI', link: '/config/model-management-ui' }, { text: 'Rag Vector Stores', link: '/config/vector-stores' }, - { text: 'Usage & Features', link: '/config/features' }, { text: 'Branding', link: '/config/branding' }, - { text: 'Hiding Advanced Chat UI Components', link: '/config/hiding-chat-components' }, + { text: 'Configure IdP: Cognito & Keycloak Examples', link: '/config/idp' }, + { text: 'LiteLLM', link: '/config/lite-llm' }, ], }, { @@ -52,9 +46,13 @@ const navLinks = [ { text: 'RAG', link: '/user/rag' }, { text: 'Context Windows', link: '/user/context-windows' }, { text: 'Model KWARGS', link: '/user/model-kwargs' }, + { text: 'Model Management UI', link: '/user/model-management-ui' }, + { text: 'Hiding Advanced Chat UI Components', link: '/user/hiding-chat-components' }, { text: 'Non-RAG in Context File Management', link: '/user/nonrag-management' }, { text: 'Prompt Engineering', link: '/user/prompt-engineering' }, { text: 'Session History', link: '/user/history' }, + { text: 'Breaking Changes', link: '/user/breaking-changes' }, + { text: 'Change Log', link: 'https://github.com/awslabs/LISA/releases' }, ], }]; diff --git a/lib/docs/admin/api.md b/lib/docs/admin/api.md deleted file mode 100644 index ad9e63e6..00000000 --- a/lib/docs/admin/api.md +++ /dev/null @@ -1,364 +0,0 @@ - -# API Usage Overview - -LISA provides robust API endpoints for managing models, both for users and administrators. These endpoints allow for operations such as listing, creating, updating, and deleting models. - -## API Gateway and ALB Endpoints - -LISA uses two primary APIs for model management: - -1. **User-facing OpenAI-Compatible API**: Available to all users for inference tasks and accessible through the LISA Serve ALB. This API provides an interface for querying and interacting with models deployed on Amazon ECS, Amazon Bedrock, or through LiteLLM. -2. **Admin-level Model Management API**: Available only to administrators through the API Gateway (APIGW). This API allows for full control of model lifecycle management, including creating, updating, and deleting models. - -### LiteLLM Routing in All Models - -Every model request is routed through LiteLLM, regardless of whether infrastructure (like ECS) is created for it. Whether deployed on ECS, external models via Bedrock, or managed through LiteLLM, all models are added to LiteLLM for traffic routing. The distinction is whether infrastructure is created (determined by request payloads), but LiteLLM integration is consistent for all models. The model management APIs will handle adding or removing model configurations from LiteLLM, and the LISA Serve endpoint will handle the inference requests against models available in LiteLLM. - -## User-facing OpenAI-Compatible API - -The OpenAI-compatible API is accessible through the LISA Serve ALB and allows users to list models available for inference tasks. Although not specifically part of the model management APIs, any model that is added or removed from LiteLLM via the model management API Gateway APIs will be reflected immediately upon queries to LiteLLM through the LISA Serve ALB. - -### Listing Models - -The `/v2/serve/models` endpoint on the LISA Serve ALB allows users to list all models available for inference in the LISA system. - -#### Request Example: - -```bash -curl -s -H 'Authorization: Bearer ' -X GET https:///v2/serve/models -``` - -#### Response Example: - -```json -{ - "data": [ - { - "id": "bedrock-embed-text-v2", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - }, - { - "id": "titan-express-v1", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - }, - { - "id": "sagemaker-amazon-mistrallite", - "object": "model", - "created": 1677610602, - "owned_by": "openai" - } - ], - "object": "list" -} -``` - -#### Explanation of Response Fields: - -These fields are all defined by the OpenAI API specification, which is documented [here](https://platform.openai.com/docs/api-reference/models/list). - -- `id`: A unique identifier for the model. -- `object`: The type of object, which is "model" in this case. -- `created`: A Unix timestamp representing when the model was created. -- `owned_by`: The entity responsible for the model, such as "openai." - -## Admin-level Model Management API - -This API is only accessible by administrators via the API Gateway and is used to create, update, and delete models. It supports full model lifecycle management. - -### Listing Models (Admin API) - -The `/models` route allows admins to list all models managed by the system. This includes models that are either creating, deleting, already active, or in a failed state. Models can be deployed via ECS or managed externally through a LiteLLM configuration. - -#### Request Example: - -```bash -curl -s -H "Authorization: Bearer " -X GET https:///models -``` - -#### Response Example: - -```json -{ - "models": [ - { - "autoScalingConfig": { - "minCapacity": 1, - "maxCapacity": 1, - "cooldown": 420, - "defaultInstanceWarmup": 180, - "metricConfig": { - "albMetricName": "RequestCountPerTarget", - "targetValue": 30, - "duration": 60, - "estimatedInstanceWarmup": 330 - } - }, - "containerConfig": { - "image": { - "baseImage": "vllm/vllm-openai:v0.5.0", - "type": "asset" - }, - "sharedMemorySize": 2048, - "healthCheckConfig": { - "command": [ - "CMD-SHELL", - "exit 0" - ], - "interval": 10, - "startPeriod": 30, - "timeout": 5, - "retries": 3 - }, - "environment": { - "MAX_TOTAL_TOKENS": "2048", - "MAX_CONCURRENT_REQUESTS": "128", - "MAX_INPUT_LENGTH": "1024" - } - }, - "loadBalancerConfig": { - "healthCheckConfig": { - "path": "/health", - "interval": 60, - "timeout": 30, - "healthyThresholdCount": 2, - "unhealthyThresholdCount": 10 - } - }, - "instanceType": "g5.xlarge", - "modelId": "mistral-vllm", - "modelName": "mistralai/Mistral-7B-Instruct-v0.2", - "modelType": "textgen", - "modelUrl": null, - "status": "Creating", - "streaming": true - }, - { - "autoScalingConfig": null, - "containerConfig": null, - "loadBalancerConfig": null, - "instanceType": null, - "modelId": "titan-express-v1", - "modelName": "bedrock/amazon.titan-text-express-v1", - "modelType": "textgen", - "modelUrl": null, - "status": "InService", - "streaming": true - } - ] -} -``` - -#### Explanation of Response Fields: - -- `modelId`: A unique identifier for the model. -- `modelName`: The name of the model, typically referencing the underlying service (Bedrock, SageMaker, etc.). -- `status`: The current state of the model, e.g., "Creating," "Active," or "Failed." -- `streaming`: Whether the model supports streaming inference. -- `instanceType` (optional): The instance type if the model is deployed via ECS. - -### Creating a Model (Admin API) - -LISA provides the `/models` endpoint for creating both ECS and LiteLLM-hosted models. Depending on the request payload, infrastructure will be created or bypassed (e.g., for LiteLLM-only models). - -This API accepts the same model definition parameters that were accepted in the V2 model definitions within the config.yaml file with one notable difference: the `containerConfig.image.path` field is -now omitted because it corresponded with the `inferenceContainer` selection. As a convenience, this path is no longer required. - -#### Request Example: - -``` -POST https:///models -``` - -#### Example Payload for ECS Model: - -```json -{ - "modelId": "mistral-vllm", - "modelName": "mistralai/Mistral-7B-Instruct-v0.2", - "modelType": "textgen", - "inferenceContainer": "vllm", - "instanceType": "g5.xlarge", - "streaming": true, - "containerConfig": { - "image": { - "baseImage": "vllm/vllm-openai:v0.5.0", - "type": "asset" - }, - "sharedMemorySize": 2048, - "environment": { - "MAX_CONCURRENT_REQUESTS": "128", - "MAX_INPUT_LENGTH": "1024", - "MAX_TOTAL_TOKENS": "2048" - }, - "healthCheckConfig": { - "command": ["CMD-SHELL", "exit 0"], - "interval": 10, - "startPeriod": 30, - "timeout": 5, - "retries": 3 - } - }, - "autoScalingConfig": { - "minCapacity": 1, - "maxCapacity": 1, - "cooldown": 420, - "defaultInstanceWarmup": 180, - "metricConfig": { - "albMetricName": "RequestCountPerTarget", - "targetValue": 30, - "duration": 60, - "estimatedInstanceWarmup": 330 - } - }, - "loadBalancerConfig": { - "healthCheckConfig": { - "path": "/health", - "interval": 60, - "timeout": 30, - "healthyThresholdCount": 2, - "unhealthyThresholdCount": 10 - } - } -} -``` - -#### Creating a LiteLLM-Only Model: - -```json -{ - "modelId": "titan-express-v1", - "modelName": "bedrock/amazon.titan-text-express-v1", - "modelType": "textgen", - "streaming": true -} -``` - -#### Explanation of Key Fields for Creation Payload: - -- `modelId`: The unique identifier for the model. This is any name you would like it to be. -- `modelName`: The name of the model as it appears in the system. For LISA-hosted models, this must be the S3 Key to your model artifacts, otherwise - this is the LiteLLM-compatible reference to a SageMaker Endpoint or Bedrock Foundation Model. Note: Bedrock and SageMaker resources must exist in the - same region as your LISA deployment. If your LISA installation is in us-east-1, then all SageMaker and Bedrock calls will also happen in us-east-1. - Configuration examples: - - LISA hosting: If your model artifacts are in `s3://${lisa_models_bucket}/path/to/model/weights`, then the `modelName` value here should be `path/to/model/weights` - - LiteLLM-only, Bedrock: If you want to use `amazon.titan-text-lite-v1`, your `modelName` value should be `bedrock/amazon.titan-text-lite-v1` - - LiteLLM-only, SageMaker: If you want to use a SageMaker Endpoint named `my-sm-endpoint`, then the `modelName` value should be `sagemaker/my-sm-endpoint`. -- `modelType`: The type of model, such as text generation (textgen). -- `streaming`: Whether the model supports streaming inference. -- `instanceType`: The type of EC2 instance to be used (only applicable for ECS models). -- `containerConfig`: Details about the Docker container, memory allocation, and environment variables. -- `autoScalingConfig`: Configuration related to ECS autoscaling. -- `loadBalancerConfig`: Health check configuration for load balancers. - -### Deleting a Model (Admin API) - -Admins can delete a model using the following endpoint. Deleting a model removes the infrastructure (ECS) or disconnects from LiteLLM. - -#### Request Example: - -``` -DELETE https:///models/{modelId} -``` - -#### Response Example: - -```json -{ - "status": "success", - "message": "Model mistral-vllm has been deleted successfully." -} -``` - -### Updating a Model - -LISA offers basic updating functionality for both LISA-hosted and LiteLLM-only models. For both types, the model type and streaming support can be updated -in the cases that the models were originally created with the wrong parameters. For example, if an embedding model was accidentally created as a `textgen` -model, the UpdateModel API can be used to set it to the intended `embedding` value. Additionally, for LISA-hosted models, users may update the AutoScaling -configuration to increase or decrease capacity usage for each model. Users may use this API to completely shut down all instances behind a model until -they want to add capacity back to the model for usage later. This feature can help users to effectively manage costs so that instances do not have to stay -running in time periods of little or no expected usage. - -The UpdateModel API has mutually exclusive payload fields to avoid conflicting requests. The API does not allow for shutting off a model at the same time -as updating its AutoScaling configuration, as these would introduce ambiguous intents. The API does not allow for setting AutoScaling limits to 0 and instead -requires the usage of the enable/disable functionality to allow models to fully scale down or turn back on. Metadata updates, such as changing the model type -or streaming compatibility, can happen in either type of update or simply by themselves. - -#### Request Example - -``` -PUT https:///models/{modelId} -``` - -#### Example Payloads - -##### Update Model Metadata - -This payload will simply update the model metadata, which will complete within seconds of invoking. If setting a model as an `embedding` model, then the -`streaming` option must be set to `false` or omitted as LISA does not support streaming with embedding models. Both the `streaming` and `modelType` options -may be included in any other update request. - -```json -{ - "streaming": true, - "modelType": "textgen" -} -``` - -##### Update AutoScaling Configuration - -This payload will update the AutoScaling configuration for minimum, maximum, and desired number of instances. The desired number must be between the -minimum or maximum numbers, inclusive, and all the numbers must be strictly greater than 0. If the model currently has less than the minimum number, then -the desired count will automatically raise to the minimum if a desired count is not specified. Despite setting a desired capacity, the model will scale down -to the minimum number over time if you are not hitting the scaling thresholds set when creating the model in the first place. - -The AutoScaling configuration **can** be updated while the model is in the Stopped state, but it won't be applied immediately. Instead, the configuration will -be saved until the model is started again, in which it will use the most recently updated AutoScaling configuration. - -The request will fail if the `autoScalingInstanceConfig` is defined at the same time as the `enabled` field. These options are mutually exclusive and must be -handled as separate operations. Any or all of the options within the `autoScalingInstanceConfig` may be set as needed, so if you only wish to change the `desiredCapacity`, -then that is the only option that you need to specify in the request object within the `autoScalingInstanceConfig`. - -```json -{ - "autoScalingInstanceConfig": { - "minCapacity": 2, - "maxCapacity": 4, - "desiredCapacity": 3 - } -} -``` - -##### Stop Model - Scale Down to 0 Instances - -This payload will stop all model EC2 instances and remove the model reference from LiteLLM so that users are unable to make inference requests against a model -with no capacity. This option is useful for users who wish to manage costs and turn off instances when the model is not currently needed but will be used again -in the future. - -The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be -handled as separate operations. - -```json -{ - "enabled": false -} -``` - -##### Start Model - Restore Previous AutoScaling Configuration - -After stopping a model, this payload will turn the model back on by spinning up instances, waiting for the expected spin-up time to allow models to initialize, and then -adding the reference back to LiteLLM so that users may query the model again. This is expected to be a much faster operation than creating the model through the CreateModel -API, so as long as the model details don't have to change, this in combination with the Stop payload will help to manage costs while still providing model availability as -quickly as the system can spin it up again. - -The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be -handled as separate operations. - -```json -{ - "enabled": true -} -``` diff --git a/lib/docs/admin/architecture.md b/lib/docs/admin/architecture.md index e0570bea..447043d9 100644 --- a/lib/docs/admin/architecture.md +++ b/lib/docs/admin/architecture.md @@ -1,31 +1,4 @@ # LLM Inference Solution for Amazon Dedicated Cloud (LISA) -![LISA Architecture](../assets/LisaArchitecture.png) -LISA is an infrastructure-as-code solution that supports model hosting and inference. Customers deploy LISA directly -into an AWS account and provision their own infrastructure. Customers bring their own models to LISA for hosting and -inference through Amazon ECS. LISA accelerates the use of Generative AI (GenAI) applications by providing scalable, -low latency access to customers’ generative LLMs and embedding language models. Customers can then focus on -experimenting with LLMs and developing GenAI applications. - -LISA’s chatbot user interface can be used for experiment with features and for production use cases. LISA enhances model -output by integrating retrieval-augmented generation (RAG) with Amazon OpenSearch or PostgreSQL’s PGVector extension, -incorporating external knowledge sources into model responses. This helps reduce the need for fine-tuning and delivers -more contextually relevant outputs. - -LISA supports OpenAI’s API Spec via the LiteLLM proxy. This means that LISA is compatible for customers to configure -with models hosted externally by supported model providers. LiteLLM also allows customers to use LISA to standardize -model orchestration and communication across model providers instead of managing each individually. With OpenAI API spec -support, LISA can also be used as a stand-in replacement for any application that already utilizes OpenAI-centric -tooling (ex: OpenAI’s Python library, LangChain). - -## Background - -LISA is a robust, AWS-native platform designed to simplify the deployment and management of Large Language Models (LLMs) in scalable, secure, and highly available environments. Drawing inspiration from the AWS open-source project [aws-genai-llm-chatbot](https://github.com/aws-samples/aws-genai-llm-chatbot), LISA builds on this foundation by offering more specialized functionality, particularly in the areas of security, modularity, and flexibility. - -One of the key differentiators of LISA is its ability to leverage the [text-generation-inference](https://github.com/huggingface/text-generation-inference/tree/main) text-generation-inference container from HuggingFace, allowing users to deploy cutting-edge LLMs. LISA also introduces several innovations that extend beyond its inspiration: - -1. **Support for Amazon Dedicated Cloud (ADC):** LISA is designed to operate in highly controlled environments like Amazon Dedicated Cloud (ADC) partitions, making it ideal for industries with stringent regulatory and security requirements. This focus on secure, isolated deployments differentiates LISA from other open-source platforms. -1. **Modular Design for Composability:** LISA's architecture is designed to be composable, splitting its components into distinct services. The core components, LISA Serve (for LLM serving and inference) and LISA Chat (for the chat interface), can be deployed as independent stacks. This modularity allows users to deploy only the parts they need, enhancing flexibility and scalability across different deployment environments. -1. **OpenAI API Specification Support:** LISA is built to support the OpenAI API specification, allowing users to replace OpenAI’s API with LISA without needing to change existing application code. This makes LISA a drop-in replacement for any workflow or application that already leverages OpenAI’s tooling, such as the OpenAI Python library or LangChain. ## System Overview @@ -39,3 +12,63 @@ LISA is designed using a modular, microservices-based architecture, where each s * **Inference Requests** served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. * **Chat Interface** enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. * **Retrieval-Augmented Generation (RAG) Operations**, leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. + +### System Architecture + +![LISA Architecture](../assets/LisaArchitecture.png) + +## LISA Components + +### LISA Model Management +![LISA Model Management Architecture](../assets/LisaModelManagement.png) +The Model Management component is responsible for managing the entire lifecycle of models in LISA. This includes creation, updating, deletion, and scaling of models deployed on ECS. The system automates and scales these operations, ensuring that the underlying infrastructure is managed efficiently. + +* **Model Hosting**: Models are containerized and deployed on AWS ECS, with each model hosted in its own isolated ECS task. This design allows models to be independently scaled based on demand. Traffic to the models is balanced using Application Load Balancers (ALBs), ensuring that the autoscaling mechanism reacts to load fluctuations in real time, optimizing both performance and availability. +* **External Model Routing**: LISA utilizes the LiteLLM proxy to route traffic to different model providers, no matter their API and payload format. Users may add models from external providers, such as SageMaker or Bedrock, to their system to allow requests to models hosted in those systems and services. LISA will simply add the configuration to LiteLLM without creating any additional supporting infrastructure. +* **Model Lifecycle Management**: AWS Step Functions are used to orchestrate the lifecycle of models, handling the creation, update, and deletion workflows. Each workflow provisions the required resources using CloudFormation templates, which manage infrastructure components like EC2 instances, security groups, and ECS services. The system ensures that the necessary security, networking, and infrastructure components are automatically deployed and configured. + * The CloudFormation stacks define essential resources using the LISA core VPC configuration, ensuring best practices for security and access across all resources in the environment. + * DynamoDB stores model metadata, while Amazon S3 securely manages model weights, enabling ECS instances to retrieve the weights dynamically during deployment. + +#### Technical Implementation + +* **Model Lifecycle**: Lifecycle operations such as creation, update, and deletion are executed by Step Functions and backed by AWS Lambda in ```lambda/models/lambda_functions.py```. +* **CloudFormation**: Infrastructure components are provisioned using CloudFormation templates, as defined in ```ecs_model_deployer/src/lib/lisa_model_stack.ts```. +* **ECS Cluster**: ECS cluster and task definitions are located in ```ecs_model_deployer/src/lib/ecsCluster.ts```, with model containers specified in ```ecs_model_deployer/src/lib/ecs-model.ts```. + + +### LISA Serve +![LISA Serve Architecture](../assets/LisaServe.png) +LISA Serve is responsible for processing inference requests and serving model predictions. This component manages user requests to interact with LLMs and ensures that the models deliver low-latency responses. + +* **Inference Requests**: Requests are routed via ALB, which serves as the main entry point to LISA’s backend infrastructure. The ALB forwards requests to the appropriate ECS-hosted model or externally-hosted model based on the request parameters. For models hosted within LISA, traffic to the models is managed with model-specific ALBs, which enable autoscaling if the models are under heavy load. LISA supports both direct REST API-based interaction and interaction through the Chat UI, enabling programmatic access or a user-friendly chat experience. +* **RAG (Retrieval-Augmented Generation)**: RAG operations enhance model responses by integrating external data sources. LISA leverages OpenSearch or PGVector (PostgreSQL) as vector stores, enabling vector-based search and retrieval of relevant knowledge to augment LLM outputs dynamically. + +#### Technical Implementation + +* RAG operations are managed through ```lambda/rag/lambda_functions.py```, which handles embedding generation and document retrieval via OpenSearch and PostgreSQL. +* Direct requests to the LISA Serve ALB entrypoint must utilize the OpenAI API spec, which we support through the use of the LiteLLM proxy. + + +### LISA Chat +![LISA Chatbot Architecture](../assets/LisaChat.png) +LISA Chat provides a customizable chat interface that enables users to interact with models in real-time. This component ensures that users have a seamless experience for submitting queries and maintaining session continuity. + +* **Chat Interface**: The Chat UI is hosted as a static website on Amazon S3 and is served via API Gateway. Users can interact with models directly through the web-based frontend, sending queries and viewing real-time responses from the models. The interface is integrated with LISA's backend services for model inference, retrieval augmented generation, and session management. +* **Session History Management**: LISA maintains session histories using DynamoDB, allowing users to retrieve and continue previous conversations seamlessly. This feature is crucial for maintaining continuity in multi-turn conversations with the models. + +#### Technical Implementation + +* The Chat UI is implemented in the ```lib/user-interface/react/``` folder and is deployed using the scripts in the ```scripts/``` folder. +* Session management logic is handled in ```lambda/session/lambda_functions.py```, where session data is stored and retrieved from DynamoDB. +* RAG operations are defined in lambda/repository/lambda_functions.py + + +## Interaction Flow + +1. **User Interaction with Chat UI or API:** Users can interact with LISA through the Chat UI or REST API. Each interaction is authenticated using AWS Cognito or OIDC, ensuring secure access. +1. **Request Routing:** The API Gateway securely routes user requests to the appropriate backend services, whether for fetching the chat UI, performing RAG operations, or managing models. +1. **Model Management:** Administrators can deploy, update, or delete models via the Model Management API, which triggers ECS deployment and scaling workflows. +1. **Model Inference:** Inference requests are routed to ECS-hosted models or external models via the LiteLLM proxy. Responses are served back to users through the ALB. +1. **RAG Integration:** When RAG is enabled, LISA retrieves relevant documents from OpenSearch or PGVector, augmenting the model's response with external knowledge. +1. **Session Continuity:** User session data is stored in DynamoDB, ensuring that users can retrieve and continue previous conversations across multiple interactions. +1. **Autoscaling:** ECS tasks automatically scale based on system load, with ALBs distributing traffic across available instances to ensure performance. diff --git a/lib/docs/admin/components.md b/lib/docs/admin/components.md deleted file mode 100644 index 3df5fda1..00000000 --- a/lib/docs/admin/components.md +++ /dev/null @@ -1,55 +0,0 @@ -## LISA Components - -### LISA Model Management -![LISA Model Management Architecture](../assets/LisaModelManagement.png) -The Model Management component is responsible for managing the entire lifecycle of models in LISA. This includes creation, updating, deletion, and scaling of models deployed on ECS. The system automates and scales these operations, ensuring that the underlying infrastructure is managed efficiently. - -* **Model Hosting**: Models are containerized and deployed on AWS ECS, with each model hosted in its own isolated ECS task. This design allows models to be independently scaled based on demand. Traffic to the models is balanced using Application Load Balancers (ALBs), ensuring that the autoscaling mechanism reacts to load fluctuations in real time, optimizing both performance and availability. -* **External Model Routing**: LISA utilizes the LiteLLM proxy to route traffic to different model providers, no matter their API and payload format. Users may add models from external providers, such as SageMaker or Bedrock, to their system to allow requests to models hosted in those systems and services. LISA will simply add the configuration to LiteLLM without creating any additional supporting infrastructure. -* **Model Lifecycle Management**: AWS Step Functions are used to orchestrate the lifecycle of models, handling the creation, update, and deletion workflows. Each workflow provisions the required resources using CloudFormation templates, which manage infrastructure components like EC2 instances, security groups, and ECS services. The system ensures that the necessary security, networking, and infrastructure components are automatically deployed and configured. - * The CloudFormation stacks define essential resources using the LISA core VPC configuration, ensuring best practices for security and access across all resources in the environment. - * DynamoDB stores model metadata, while Amazon S3 securely manages model weights, enabling ECS instances to retrieve the weights dynamically during deployment. - -#### Technical Implementation - -* **Model Lifecycle**: Lifecycle operations such as creation, update, and deletion are executed by Step Functions and backed by AWS Lambda in ```lambda/models/lambda_functions.py```. -* **CloudFormation**: Infrastructure components are provisioned using CloudFormation templates, as defined in ```ecs_model_deployer/src/lib/lisa_model_stack.ts```. -* **ECS Cluster**: ECS cluster and task definitions are located in ```ecs_model_deployer/src/lib/ecsCluster.ts```, with model containers specified in ```ecs_model_deployer/src/lib/ecs-model.ts```. - - -### LISA Serve -![LISA Serve Architecture](../assets/LisaServe.png) -LISA Serve is responsible for processing inference requests and serving model predictions. This component manages user requests to interact with LLMs and ensures that the models deliver low-latency responses. - -* **Inference Requests**: Requests are routed via ALB, which serves as the main entry point to LISA’s backend infrastructure. The ALB forwards requests to the appropriate ECS-hosted model or externally-hosted model based on the request parameters. For models hosted within LISA, traffic to the models is managed with model-specific ALBs, which enable autoscaling if the models are under heavy load. LISA supports both direct REST API-based interaction and interaction through the Chat UI, enabling programmatic access or a user-friendly chat experience. -* **RAG (Retrieval-Augmented Generation)**: RAG operations enhance model responses by integrating external data sources. LISA leverages OpenSearch or PGVector (PostgreSQL) as vector stores, enabling vector-based search and retrieval of relevant knowledge to augment LLM outputs dynamically. - -#### Technical Implementation - -* RAG operations are managed through ```lambda/rag/lambda_functions.py```, which handles embedding generation and document retrieval via OpenSearch and PostgreSQL. -* Direct requests to the LISA Serve ALB entrypoint must utilize the OpenAI API spec, which we support through the use of the LiteLLM proxy. - - -### LISA Chat -![LISA Chatbot Architecture](../assets/LisaChat.png) -LISA Chat provides a customizable chat interface that enables users to interact with models in real-time. This component ensures that users have a seamless experience for submitting queries and maintaining session continuity. - -* **Chat Interface**: The Chat UI is hosted as a static website on Amazon S3 and is served via API Gateway. Users can interact with models directly through the web-based frontend, sending queries and viewing real-time responses from the models. The interface is integrated with LISA's backend services for model inference, retrieval augmented generation, and session management. -* **Session History Management**: LISA maintains session histories using DynamoDB, allowing users to retrieve and continue previous conversations seamlessly. This feature is crucial for maintaining continuity in multi-turn conversations with the models. - -#### Technical Implementation - -* The Chat UI is implemented in the ```lib/user-interface/react/``` folder and is deployed using the scripts in the ```scripts/``` folder. -* Session management logic is handled in ```lambda/session/lambda_functions.py```, where session data is stored and retrieved from DynamoDB. -* RAG operations are defined in lambda/repository/lambda_functions.py - - -## Interaction Flow - -1. **User Interaction with Chat UI or API:** Users can interact with LISA through the Chat UI or REST API. Each interaction is authenticated using AWS Cognito or OIDC, ensuring secure access. -1. **Request Routing:** The API Gateway securely routes user requests to the appropriate backend services, whether for fetching the chat UI, performing RAG operations, or managing models. -1. **Model Management:** Administrators can deploy, update, or delete models via the Model Management API, which triggers ECS deployment and scaling workflows. -1. **Model Inference:** Inference requests are routed to ECS-hosted models or external models via the LiteLLM proxy. Responses are served back to users through the ALB. -1. **RAG Integration:** When RAG is enabled, LISA retrieves relevant documents from OpenSearch or PGVector, augmenting the model's response with external knowledge. -1. **Session Continuity:** User session data is stored in DynamoDB, ensuring that users can retrieve and continue previous conversations across multiple interactions. -1. **Autoscaling:** ECS tasks automatically scale based on system load, with ALBs distributing traffic across available instances to ensure performance. diff --git a/lib/docs/admin/getting-started.md b/lib/docs/admin/getting-started.md index 7358be69..af66e3c2 100644 --- a/lib/docs/admin/getting-started.md +++ b/lib/docs/admin/getting-started.md @@ -17,7 +17,7 @@ Before beginning, ensure you have: 3. Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles 4. Python 3.9 or later 5. Node.js 14 or later -6. Docker installed and running +6. Docker/Finch installed and running 7. Sufficient disk space for model downloads and conversions If you're new to CDK, review the [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) and consult with your AWS support team. @@ -48,6 +48,7 @@ Set the following environment variables: export PROFILE=my-aws-profile # Optional, can be left blank export DEPLOYMENT_NAME=my-deployment export ENV=dev # Options: dev, test, or prod +export CDK_DOCKER=finch # Optional, only required if not using docker as container engine ``` ## Step 3: Set Up Python and TypeScript Environments @@ -116,7 +117,10 @@ make modelCheck This command verifies if the model's weights are already present in your S3 bucket. If not, it downloads the weights, converts them to the required format, and uploads them to your S3 bucket. Ensure adequate disk space is available for this process. > **WARNING** -> As of LISA 3.0, the `ecsModels` parameter in `config.yaml` is solely for staging model weights in your S3 bucket. Previously, before models could be managed through the [API](https://github.com/awslabs/LISA/blob/develop/README.md#creating-a-model-admin-api) or via the Model Management section of the [Chatbot](https://github.com/awslabs/LISA/blob/develop/README.md#chatbot-example), this parameter also dictated which models were deployed. +> As of LISA 3.0, the `ecsModels` parameter in `config.yaml` is solely for staging model weights in your S3 bucket. +> Previously, before models could be managed through the [API](/admin/model-management) or via the Model Management +> section of the [Chatbot](/user/chat), this parameter also +> dictated which models were deployed. > **NOTE** > For air-gapped systems, before running `make modelCheck` you should manually download model artifacts and place them in a `models` directory at the project root, using the structure: `models/`. @@ -134,38 +138,14 @@ In the `config.yaml` file, configure the `authConfig` block for authentication. - `jwtGroupsProperty`: Path to the groups field in the JWT token - `additionalScopes` (optional): Extra scopes for group membership information -#### Cognito Configuration Example: -In Cognito, the `authority` will be the URL to your User Pool. As an example, if your User Pool ID, not the name, is `us-east-1_example`, and if it is -running in `us-east-1`, then the URL to put in the `authority` field would be `https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example`. The `clientId` -can be found in your User Pool's "App integration" tab from within the AWS Management Console, and at the bottom of the page, you will see the list of clients -and their associated Client IDs. The ID here is what we will need for the `clientId` field. +IDP Configuration examples using AWS Cognito and Keycloak can be found: [IDP Configuration Examples](/config/idp) -```yaml -authConfig: - authority: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example - clientId: your-client-id - adminGroup: AdminGroup - jwtGroupsProperty: cognito:groups -``` - -#### Keycloak Configuration Example: -In Keycloak, the `authority` will be the URL to your Keycloak server. The `clientId` is likely not a random string like in the Cognito clients, and instead -will be a string configured by your Keycloak administrator. Your administrator will be able to give you a client name or create a client for you to use for -this application. Once you have this string, use that as the `clientId` within the `authConfig` block. - -```yaml -authConfig: - authority: https://your-keycloak-server.com - clientId: your-client-name - adminGroup: AdminGroup - jwtGroupsProperty: realm_access.roles -``` - ## Step 7: Configure LiteLLM We utilize LiteLLM under the hood to allow LISA to respond to the [OpenAI specification](https://platform.openai.com/docs/api-reference). For LiteLLM configuration, a key must be set up so that the system may communicate with a database for tracking all the models that are added or removed -using the [Model Management API](#admin-level-model-management-api). The key must start with `sk-` and then can be any arbitrary string. We recommend generating a new UUID and then using that as +using the [Model Management API](/admin/model-management). The key must start with `sk-` and then can be any arbitrary +string. We recommend generating a new UUID and then using that as the key. Configuration example is below. @@ -177,7 +157,8 @@ litellmConfig: ``` **Note**: It is possible to add LiteLLM-only models to this configuration, but it is not recommended as the models in this configuration will not show in the -Chat or Model Management UIs. Instead, use the [Model Management UI](#admin-level-model-management-api) to add or remove LiteLLM-only model configurations. +Chat or Model Management UIs. Instead, use the [Model Management UI](/user/models) to add or remove LiteLLM-only model +configurations. ## Step 8: Set Up SSL Certificates (Development Only) @@ -203,7 +184,8 @@ restApiConfig: In the `ecsModels` section of `config.yaml`, allow our deployment process to pull the model weights for you. During the deployment process, LISA will optionally attempt to download your model weights if you specify an optional `ecsModels` -array, this will only work in non ADC regions. Specifically, see the `ecsModels` section of the [example_config.yaml](./example_config.yaml) file. +array, this will only work in non ADC regions. Specifically, see the `ecsModels` section of +the [example_config.yaml](https://github.com/awslabs/LISA/blob/develop/example_config.yaml) file. Here we define the model name, inference container, and baseImage: ```yaml @@ -252,7 +234,8 @@ services are in the same region as the LISA installation, LISA can use them alon **Important:** Endpoints or Models statically defined during LISA deployment cannot be removed or updated using the LISA Model Management API, and they will not show in the Chat UI. These will only show as part of the OpenAI `/models` API. -Although there is support for it, we recommend using the [Model Management API](#admin-level-model-management-api) instead of the following static configuration. +Although there is support for it, we recommend using the [Model Management API](/admin/model-management) instead of the +following static configuration. ### Example Configuration diff --git a/lib/docs/admin/idp.md b/lib/docs/admin/idp.md deleted file mode 100644 index 46409041..00000000 --- a/lib/docs/admin/idp.md +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/lib/docs/admin/model-management.md b/lib/docs/admin/model-management.md index 46409041..cb4b7ba9 100644 --- a/lib/docs/admin/model-management.md +++ b/lib/docs/admin/model-management.md @@ -1 +1,364 @@ -# TODO + +# Model Management API Usage + +LISA provides robust API endpoints for managing models, both for users and administrators. These endpoints allow for operations such as listing, creating, updating, and deleting models. + +## API Gateway and ALB Endpoints + +LISA uses two primary APIs for model management: + +1. **User-facing OpenAI-Compatible API**: Available to all users for inference tasks and accessible through the LISA Serve ALB. This API provides an interface for querying and interacting with models deployed on Amazon ECS, Amazon Bedrock, or through LiteLLM. +2. **Admin-level Model Management API**: Available only to administrators through the API Gateway (APIGW). This API allows for full control of model lifecycle management, including creating, updating, and deleting models. + +### LiteLLM Routing in All Models + +Every model request is routed through LiteLLM, regardless of whether infrastructure (like ECS) is created for it. Whether deployed on ECS, external models via Bedrock, or managed through LiteLLM, all models are added to LiteLLM for traffic routing. The distinction is whether infrastructure is created (determined by request payloads), but LiteLLM integration is consistent for all models. The model management APIs will handle adding or removing model configurations from LiteLLM, and the LISA Serve endpoint will handle the inference requests against models available in LiteLLM. + +## User-facing OpenAI-Compatible API + +The OpenAI-compatible API is accessible through the LISA Serve ALB and allows users to list models available for inference tasks. Although not specifically part of the model management APIs, any model that is added or removed from LiteLLM via the model management API Gateway APIs will be reflected immediately upon queries to LiteLLM through the LISA Serve ALB. + +### Listing Models + +The `/v2/serve/models` endpoint on the LISA Serve ALB allows users to list all models available for inference in the LISA system. + +#### Request Example: + +```bash +curl -s -H 'Authorization: Bearer ' -X GET https:///v2/serve/models +``` + +#### Response Example: + +```json +{ + "data": [ + { + "id": "bedrock-embed-text-v2", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "titan-express-v1", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + }, + { + "id": "sagemaker-amazon-mistrallite", + "object": "model", + "created": 1677610602, + "owned_by": "openai" + } + ], + "object": "list" +} +``` + +#### Explanation of Response Fields: + +These fields are all defined by the OpenAI API specification, which is documented [here](https://platform.openai.com/docs/api-reference/models/list). + +- `id`: A unique identifier for the model. +- `object`: The type of object, which is "model" in this case. +- `created`: A Unix timestamp representing when the model was created. +- `owned_by`: The entity responsible for the model, such as "openai." + +## Admin-level Model Management API + +This API is only accessible by administrators via the API Gateway and is used to create, update, and delete models. It supports full model lifecycle management. + +### Listing Models (Admin API) + +The `/models` route allows admins to list all models managed by the system. This includes models that are either creating, deleting, already active, or in a failed state. Models can be deployed via ECS or managed externally through a LiteLLM configuration. + +#### Request Example: + +```bash +curl -s -H "Authorization: Bearer " -X GET https:///models +``` + +#### Response Example: + +```json +{ + "models": [ + { + "autoScalingConfig": { + "minCapacity": 1, + "maxCapacity": 1, + "cooldown": 420, + "defaultInstanceWarmup": 180, + "metricConfig": { + "albMetricName": "RequestCountPerTarget", + "targetValue": 30, + "duration": 60, + "estimatedInstanceWarmup": 330 + } + }, + "containerConfig": { + "image": { + "baseImage": "vllm/vllm-openai:v0.5.0", + "type": "asset" + }, + "sharedMemorySize": 2048, + "healthCheckConfig": { + "command": [ + "CMD-SHELL", + "exit 0" + ], + "interval": 10, + "startPeriod": 30, + "timeout": 5, + "retries": 3 + }, + "environment": { + "MAX_TOTAL_TOKENS": "2048", + "MAX_CONCURRENT_REQUESTS": "128", + "MAX_INPUT_LENGTH": "1024" + } + }, + "loadBalancerConfig": { + "healthCheckConfig": { + "path": "/health", + "interval": 60, + "timeout": 30, + "healthyThresholdCount": 2, + "unhealthyThresholdCount": 10 + } + }, + "instanceType": "g5.xlarge", + "modelId": "mistral-vllm", + "modelName": "mistralai/Mistral-7B-Instruct-v0.2", + "modelType": "textgen", + "modelUrl": null, + "status": "Creating", + "streaming": true + }, + { + "autoScalingConfig": null, + "containerConfig": null, + "loadBalancerConfig": null, + "instanceType": null, + "modelId": "titan-express-v1", + "modelName": "bedrock/amazon.titan-text-express-v1", + "modelType": "textgen", + "modelUrl": null, + "status": "InService", + "streaming": true + } + ] +} +``` + +#### Explanation of Response Fields: + +- `modelId`: A unique identifier for the model. +- `modelName`: The name of the model, typically referencing the underlying service (Bedrock, SageMaker, etc.). +- `status`: The current state of the model, e.g., "Creating," "Active," or "Failed." +- `streaming`: Whether the model supports streaming inference. +- `instanceType` (optional): The instance type if the model is deployed via ECS. + +### Creating a Model (Admin API) + +LISA provides the `/models` endpoint for creating both ECS and LiteLLM-hosted models. Depending on the request payload, infrastructure will be created or bypassed (e.g., for LiteLLM-only models). + +This API accepts the same model definition parameters that were accepted in the V2 model definitions within the config.yaml file with one notable difference: the `containerConfig.image.path` field is +now omitted because it corresponded with the `inferenceContainer` selection. As a convenience, this path is no longer required. + +#### Request Example: + +``` +POST https:///models +``` + +#### Example Payload for ECS Model: + +```json +{ + "modelId": "mistral-vllm", + "modelName": "mistralai/Mistral-7B-Instruct-v0.2", + "modelType": "textgen", + "inferenceContainer": "vllm", + "instanceType": "g5.xlarge", + "streaming": true, + "containerConfig": { + "image": { + "baseImage": "vllm/vllm-openai:v0.5.0", + "type": "asset" + }, + "sharedMemorySize": 2048, + "environment": { + "MAX_CONCURRENT_REQUESTS": "128", + "MAX_INPUT_LENGTH": "1024", + "MAX_TOTAL_TOKENS": "2048" + }, + "healthCheckConfig": { + "command": ["CMD-SHELL", "exit 0"], + "interval": 10, + "startPeriod": 30, + "timeout": 5, + "retries": 3 + } + }, + "autoScalingConfig": { + "minCapacity": 1, + "maxCapacity": 1, + "cooldown": 420, + "defaultInstanceWarmup": 180, + "metricConfig": { + "albMetricName": "RequestCountPerTarget", + "targetValue": 30, + "duration": 60, + "estimatedInstanceWarmup": 330 + } + }, + "loadBalancerConfig": { + "healthCheckConfig": { + "path": "/health", + "interval": 60, + "timeout": 30, + "healthyThresholdCount": 2, + "unhealthyThresholdCount": 10 + } + } +} +``` + +#### Creating a LiteLLM-Only Model: + +```json +{ + "modelId": "titan-express-v1", + "modelName": "bedrock/amazon.titan-text-express-v1", + "modelType": "textgen", + "streaming": true +} +``` + +#### Explanation of Key Fields for Creation Payload: + +- `modelId`: The unique identifier for the model. This is any name you would like it to be. +- `modelName`: The name of the model as it appears in the system. For LISA-hosted models, this must be the S3 Key to your model artifacts, otherwise + this is the LiteLLM-compatible reference to a SageMaker Endpoint or Bedrock Foundation Model. Note: Bedrock and SageMaker resources must exist in the + same region as your LISA deployment. If your LISA installation is in us-east-1, then all SageMaker and Bedrock calls will also happen in us-east-1. + Configuration examples: + - LISA hosting: If your model artifacts are in `s3://${lisa_models_bucket}/path/to/model/weights`, then the `modelName` value here should be `path/to/model/weights` + - LiteLLM-only, Bedrock: If you want to use `amazon.titan-text-lite-v1`, your `modelName` value should be `bedrock/amazon.titan-text-lite-v1` + - LiteLLM-only, SageMaker: If you want to use a SageMaker Endpoint named `my-sm-endpoint`, then the `modelName` value should be `sagemaker/my-sm-endpoint`. +- `modelType`: The type of model, such as text generation (textgen). +- `streaming`: Whether the model supports streaming inference. +- `instanceType`: The type of EC2 instance to be used (only applicable for ECS models). +- `containerConfig`: Details about the Docker container, memory allocation, and environment variables. +- `autoScalingConfig`: Configuration related to ECS autoscaling. +- `loadBalancerConfig`: Health check configuration for load balancers. + +### Deleting a Model (Admin API) + +Admins can delete a model using the following endpoint. Deleting a model removes the infrastructure (ECS) or disconnects from LiteLLM. + +#### Request Example: + +``` +DELETE https:///models/{modelId} +``` + +#### Response Example: + +```json +{ + "status": "success", + "message": "Model mistral-vllm has been deleted successfully." +} +``` + +### Updating a Model + +LISA offers basic updating functionality for both LISA-hosted and LiteLLM-only models. For both types, the model type and streaming support can be updated +in the cases that the models were originally created with the wrong parameters. For example, if an embedding model was accidentally created as a `textgen` +model, the UpdateModel API can be used to set it to the intended `embedding` value. Additionally, for LISA-hosted models, users may update the AutoScaling +configuration to increase or decrease capacity usage for each model. Users may use this API to completely shut down all instances behind a model until +they want to add capacity back to the model for usage later. This feature can help users to effectively manage costs so that instances do not have to stay +running in time periods of little or no expected usage. + +The UpdateModel API has mutually exclusive payload fields to avoid conflicting requests. The API does not allow for shutting off a model at the same time +as updating its AutoScaling configuration, as these would introduce ambiguous intents. The API does not allow for setting AutoScaling limits to 0 and instead +requires the usage of the enable/disable functionality to allow models to fully scale down or turn back on. Metadata updates, such as changing the model type +or streaming compatibility, can happen in either type of update or simply by themselves. + +#### Request Example + +``` +PUT https:///models/{modelId} +``` + +#### Example Payloads + +##### Update Model Metadata + +This payload will simply update the model metadata, which will complete within seconds of invoking. If setting a model as an `embedding` model, then the +`streaming` option must be set to `false` or omitted as LISA does not support streaming with embedding models. Both the `streaming` and `modelType` options +may be included in any other update request. + +```json +{ + "streaming": true, + "modelType": "textgen" +} +``` + +##### Update AutoScaling Configuration + +This payload will update the AutoScaling configuration for minimum, maximum, and desired number of instances. The desired number must be between the +minimum or maximum numbers, inclusive, and all the numbers must be strictly greater than 0. If the model currently has less than the minimum number, then +the desired count will automatically raise to the minimum if a desired count is not specified. Despite setting a desired capacity, the model will scale down +to the minimum number over time if you are not hitting the scaling thresholds set when creating the model in the first place. + +The AutoScaling configuration **can** be updated while the model is in the Stopped state, but it won't be applied immediately. Instead, the configuration will +be saved until the model is started again, in which it will use the most recently updated AutoScaling configuration. + +The request will fail if the `autoScalingInstanceConfig` is defined at the same time as the `enabled` field. These options are mutually exclusive and must be +handled as separate operations. Any or all of the options within the `autoScalingInstanceConfig` may be set as needed, so if you only wish to change the `desiredCapacity`, +then that is the only option that you need to specify in the request object within the `autoScalingInstanceConfig`. + +```json +{ + "autoScalingInstanceConfig": { + "minCapacity": 2, + "maxCapacity": 4, + "desiredCapacity": 3 + } +} +``` + +##### Stop Model - Scale Down to 0 Instances + +This payload will stop all model EC2 instances and remove the model reference from LiteLLM so that users are unable to make inference requests against a model +with no capacity. This option is useful for users who wish to manage costs and turn off instances when the model is not currently needed but will be used again +in the future. + +The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be +handled as separate operations. + +```json +{ + "enabled": false +} +``` + +##### Start Model - Restore Previous AutoScaling Configuration + +After stopping a model, this payload will turn the model back on by spinning up instances, waiting for the expected spin-up time to allow models to initialize, and then +adding the reference back to LiteLLM so that users may query the model again. This is expected to be a much faster operation than creating the model through the CreateModel +API, so as long as the model details don't have to change, this in combination with the Stop payload will help to manage costs while still providing model availability as +quickly as the system can spin it up again. + +The request will fail if the `enabled` field is defined at the same time as the `autoScalingInstanceConfig` field. These options are mutually exclusive and must be +handled as separate operations. + +```json +{ + "enabled": true +} +``` diff --git a/lib/docs/admin/overview.md b/lib/docs/admin/overview.md new file mode 100644 index 00000000..d1857b0d --- /dev/null +++ b/lib/docs/admin/overview.md @@ -0,0 +1,63 @@ +# What is LISA? + +LISA is an infrastructure-as-code solution providing scalable, low latency access to customers’ generative LLMs and +embedding language models. LISA accelerates and supports customers’ GenAI experimentation and adoption, particularly in +regions where Amazon Bedrock is not available. LISA allows customers to move quickly rather than independently solve the +undifferentiated heavy lifting of hosting and inference architecture. Customers deploy LISA into a single AWS account +and integrate it with an identity provider. Customers bring their own models to LISA for self-hosting and inference +supported by Amazon Elastic Container Service (ECS). Model configuration is managed through LISA’s model management +APIs. + +As use cases and model requirements grow, customers can configure LISA with external model providers. Through OpenAI's +API spec via the LiteLLM proxy, LISA is compatible with 100+ models from various providers, including Amazon Bedrock and +Amazon Jumpstart. LISA customers can centralize communication across many model providers via LiteLLM, leveraging LISA +for model orchestration. Using LISA as a model orchestration layer allows customers to standardize integrations with +externally hosted models in a single place. Without an orchestration layer, customers must individually manage unique +API integrations with each provider. + +## Key Features + +* Self Host Models: Bring your own text generation and embedding models to LISA for hosting and inference. +* Model Orchestration: Centralize and standardize configuration with 100+ models from model providers via LiteLLM, + including Amazon Bedrock models. +* Chatbot User Interface: Through the chatbot user interface, users can prompt LLMs, receive responses, modify prompt + templates, change model arguments, and manage their session history. Administrators can control available features via + the configuration page. +* Retrieval-augmented generation (RAG): RAG reduces the need for fine-tuning, an expensive and time-consuming + undertaking, and delivers more contextually relevant outputs. LISA offers RAG through Amazon OpenSearch or + PostgreSQL’s PGVector extension on Amazon RDS. +* Non-RAG Model Context: Users can upload documents to their chat sessions to enhance responses or support use cases + like document summarization. +* Model Management: Administrators can add, remove, and update models configured with LISA through the model management + configuration page or APIs. +* OpenAI API spec: LISA can be configured with compatible tooling. For example, customers can configure LISA as the + model provider for the Continue plugin, an open-source AI code assistance for JetBrains and Visual Studio Code + integrated development environments (IDEs). This allows users to select from any LISA-configured model to support LLM + prompting directly in their IDE. +* Libraries: If your workflow includes libraries such as [LangChain](https://python.langchain.com/) or [OpenAI](https://github.com/openai/openai-python), then you + can place LISA in your + application by changing only the endpoint and headers for the client objects. +* FedRAMP: The AWS services that LISA leverages are FedRAMP High compliant. +* Ongoing Releases: We offer on-going release with new functionality. LISA’s roadmap is customer driven. + +## Deployment Prerequisites + +### Pre-Deployment Steps + +* Set up and have access to an AWS account with appropriate permissions + * All the resource creation that happens as part of CDK deployments expects Administrator or Administrator-like + permissions with resource creation and mutation permissions. Installation will not succeed if this profile does + not have permissions to create and edit arbitrary resources for the system. Note: This level of permissions is not + required for the runtime of LISA. This is only necessary for deployment and subsequent updates. +* Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles +* Optional: If using the chat UI, Have your Identity Provider (IdP) information and access +* Optional: Have your VPC information available, if you are using an existing one for your deployment +* Note: CDK briefly leverages SSM. Confirm it is approved for use by your organization before beginning. + +### Software + +* AWS CLI installed and configured +* Python 3.9 or later +* Node.js 14 or later +* Docker installed and running +* Sufficient disk space for model downloads and conversions diff --git a/lib/docs/admin/security.md b/lib/docs/admin/security.md deleted file mode 100644 index 46409041..00000000 --- a/lib/docs/admin/security.md +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/lib/docs/config/api-tokens.md b/lib/docs/config/api-tokens.md deleted file mode 100644 index 1a0afa75..00000000 --- a/lib/docs/config/api-tokens.md +++ /dev/null @@ -1,77 +0,0 @@ -## Programmatic API Tokens - -The LISA Serve ALB can be used for programmatic access outside the example Chat application. -An example use case would be for allowing LISA to serve LLM requests that originate from the [Continue VSCode Plugin](https://www.continue.dev/). -To facilitate communication directly with the LISA Serve ALB, a user with sufficient DynamoDB PutItem permissions may add -API keys to the APITokenTable, and once created, a user may make requests by including the `Authorization: Bearer ${token}` -header or the `Api-Key: ${token}` header with that token. If using any OpenAI-compatible library, the `api_key` fields -will use the `Authorization: Bearer ${token}` format automatically, so there is no need to include additional headers -when using those libraries. - -### Adding a Token - -An account owner may create a long-lived API Token using the following AWS CLI command. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" # change to a unique string for a user -aws --region $AWS_REGION dynamodb put-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --item '{"token": {"S": "'${token_string}'"}}' -``` - -If an account owner wants the API Token to be temporary and expire after a specific date, LISA will allow for this too. -In addition to the `token` field, the owner may specify the `tokenExpiration` field, which accepts a UNIX timestamp, -in seconds. The following command shows an example of how to do this. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" -token_expiration=$(echo $(date +%s) + 3600 | bc) # token that expires in one hour, 3600 seconds -aws --region $AWS_REGION dynamodb put-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --item '{ - "token": {"S": "'${token_string}'"}, - "tokenExpiration": {"N": "'${token_expiration}'"} - }' -``` - -Once the token is inserted into the DynamoDB Table, a user may use the token in the `Authorization` request header like -in the following snippet. - -```bash -lisa_serve_rest_url="https://" -token_string="YOUR_STRING_HERE" -curl ${lisa_serve_rest_url}/v2/serve/models \ - -H 'accept: application/json' \ - -H 'Content-Type: application/json' \ - -H "Authorization: Bearer ${token_string}" -``` - -### Updating a Token - -In the case that an owner wishes to change an existing expiration time or add one to a key that did not previously have -an expiration, this can be accomplished by editing the existing item. The following commands can be used as an example -for updating an existing token. Setting the expiration time to a time in the past will effectively remove access for -that key. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" -token_expiration=$(echo $(date +%s) + 600 | bc) # token that expires in 10 minutes from now -aws --region $AWS_REGION dynamodb update-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --key '{"token": {"S": "'${token_string}'"}}' \ - --update-expression 'SET tokenExpiration=:t' \ - --expression-attribute-values '{":t": {"N": "'${token_expiration}'"}}' -``` - -### Removing a Token - -Tokens will not be automatically removed even if they are no longer valid. An owner may remove an key, expired or not, -from the database to fully revoke the key, by deleting the item. As an example, the following commands can be used to -remove a token. - -```bash -AWS_REGION="us-east-1" # change to your deployment region -token_string="YOUR_STRING_HERE" # change to the token to remove -aws --region $AWS_REGION dynamodb delete-item --table-name $DEPLOYMENT_NAME-LISAApiTokenTable \ - --key '{"token": {"S": "'${token_string}'"}}' -``` diff --git a/lib/docs/config/branding.md b/lib/docs/config/branding.md index 46409041..47ea59d6 100644 --- a/lib/docs/config/branding.md +++ b/lib/docs/config/branding.md @@ -1 +1,13 @@ -# TODO +# Branding + +LISA supports some branding through the deployment [Configuration](/config/configuration). Adding the following +will modify the Chat UI with a custom banner: + +```yaml +systemBanner: + text: LISA System + backgroundColor: orange + fontColor: black +``` + +These values are shared across all users. diff --git a/lib/docs/config/configuration.md b/lib/docs/config/configuration.md new file mode 100644 index 00000000..0c7ea963 --- /dev/null +++ b/lib/docs/config/configuration.md @@ -0,0 +1,15 @@ +# Minimal Configuration + +Configurations for LISA are split into 2 configuration files, base and custom. The base configuration contains the +minimal properties required to deploy LISA. The file is located at the root of your project (./config-base.yaml) and +contains the following properties: + +```yaml +accountNumber: +region: +restApiConfig: +s3BucketModels: +mountS3DebUrl: +``` + + diff --git a/lib/docs/config/features.md b/lib/docs/config/features.md deleted file mode 100644 index 46409041..00000000 --- a/lib/docs/config/features.md +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/lib/docs/config/idp.md b/lib/docs/config/idp.md new file mode 100644 index 00000000..b0771014 --- /dev/null +++ b/lib/docs/config/idp.md @@ -0,0 +1,35 @@ +# IDP Configuration Examples + +## AWS Cognito Example: + +In Cognito, the `authority` will be the URL to your User Pool. As an example, if your User Pool ID, not the name, is +`us-east-1_example`, and if it is +running in `us-east-1`, then the URL to put in the `authority` field would be +`https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example`. The `clientId` +can be found in your User Pool's "App integration" tab from within the AWS Management Console, and at the bottom of the +page, you will see the list of clients +and their associated Client IDs. The ID here is what we will need for the `clientId` field. + +```yaml +authConfig: + authority: https://cognito-idp.us-east-1.amazonaws.com/us-east-1_example + clientId: your-client-id + adminGroup: AdminGroup + jwtGroupsProperty: cognito:groups +``` + +## Keycloak Example: + +In Keycloak, the `authority` will be the URL to your Keycloak server. The `clientId` is likely not a random string like +in the Cognito clients, and instead +will be a string configured by your Keycloak administrator. Your administrator will be able to give you a client name or +create a client for you to use for +this application. Once you have this string, use that as the `clientId` within the `authConfig` block. + +```yaml +authConfig: + authority: https://your-keycloak-server.com + clientId: your-client-name + adminGroup: AdminGroup + jwtGroupsProperty: realm_access.roles +``` diff --git a/lib/docs/admin/lite-llm.md b/lib/docs/config/lite-llm.md similarity index 100% rename from lib/docs/admin/lite-llm.md rename to lib/docs/config/lite-llm.md diff --git a/lib/docs/config/model-compatibility.md b/lib/docs/config/model-compatibility.md index 1f28b61e..8f7ea9b4 100644 --- a/lib/docs/config/model-compatibility.md +++ b/lib/docs/config/model-compatibility.md @@ -16,11 +16,15 @@ AutoModelForSeq2SeqLM.from_pretrained(, device_map="auto") ### HuggingFace Embedding Models -Embedding models often utilize custom codebases and are not as uniform as generation models. For this reason you will likely need to create a new `inferenceContainer`. Follow the [example](https://github.com/awslabs/LISA/tree/develop/lib/serve/ecs-model/embedding/instructor) provided for the `instructor` model. +Embedding models often utilize custom codebases and are not as uniform as generation models. For this reason you will +likely need to create a new `inferenceContainer`. Follow +the [example](https://github.com/awslabs/LISA/blob/develop/lib/serve/ecs-model/embedding/instructor) provided for the +`instructor` model. ### vLLM Models In addition to the support we have for the TGI and TEI containers, we support hosting models using the [vLLM container](https://docs.vllm.ai/en/latest/). vLLM abides by the OpenAI specification, and as such allows both text generation and embedding on the models that vLLM supports. -See the [deployment](#deployment) section for details on how to set up the vLLM container for your models. Similar to how the HuggingFace containers will serve safetensor weights downloaded from the +See the [deployment](/admin/deploy) section for details on how to set up the vLLM container for your models. Similar to +how the HuggingFace containers will serve safetensor weights downloaded from the HuggingFace website, vLLM will do the same, and our configuration will allow you to serve these artifacts automatically. vLLM does not have many supported models for embeddings, but as they become available, LISA will support them as long as the vLLM container version is updated in the config.yaml file and as long as the model's safetensors can be found in S3. diff --git a/lib/docs/config/model-management-api.md b/lib/docs/config/model-management-api.md deleted file mode 100644 index 46409041..00000000 --- a/lib/docs/config/model-management-api.md +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/lib/docs/package.json b/lib/docs/package.json index 7441aa18..f794a384 100644 --- a/lib/docs/package.json +++ b/lib/docs/package.json @@ -4,7 +4,8 @@ "version": "1.0.0", "description": "Documentation of LISA", "scripts": { - "build": "vitepress build .", + "prebuild": "(cd ../../ && npm run generateSchemaDocs)", + "build": "npm run docs:build", "docs:dev": "vitepress dev .", "docs:build": "vitepress build .", "docs:preview": "vitepress preview ." diff --git a/lib/docs/user/breaking-changes.md b/lib/docs/user/breaking-changes.md new file mode 100644 index 00000000..7b64c814 --- /dev/null +++ b/lib/docs/user/breaking-changes.md @@ -0,0 +1,63 @@ +# Breaking Changes + +## v2 to v3 Migration + +With the release of LISA v3.0.0, we have introduced several architectural changes that are incompatible with previous +versions. Although these changes may cause some friction for existing users, they aim to simplify the deployment +experience and enhance long-term scalability. The following breaking changes are critical for existing users planning to +upgrade: + +1. Model Deletion Upon Upgrade: Models deployed via EC2 and ECS using the config.yaml file’s ecsModels list will be + deleted during the upgrade process. LISA has migrated to a new model deployment system that manages models + internally, rendering the ecsModels list obsolete. We recommend backing up your model settings to facilitate their + redeployment through the new Model Management API with minimal downtime. +1. Networking Changes and Full Teardown: Core networking changes require a complete teardown of the existing LISA + installation using the make destroy command before upgrading. Cross-stack dependencies have been modified, + necessitating this full teardown to ensure proper application of the v3 infrastructure changes. Additionally, users + may need to manually delete some resources, such as ECR repositories or S3 buckets, if they were populated before + CloudFormation began deleting the stack. This operation is destructive and irreversible, so it is crucial to back up + any critical configurations and data (e.g., S3 RAG bucket contents, DynamoDB token tables) before proceeding with the + upgrade. +1. New LiteLLM Admin Key Requirement: The new Model Management API requires an "admin" key for LiteLLM to track models + for inference requests. This key, while transparent to users, must be present and conform to the required format ( + starting with sk-). The key is defined in the config.yaml file, and the LISA schema validator will prompt an error if + it is missing or incorrectly formatted. + +## v3.0.0 to v3.1.0 + +In preparation of the v3.1.0 release, there are several changes that we needed to make in order to ensure the stability +of the LISA system. + +1. The CreateModel API `containerConfig` object has been changed so that the Docker Image repository is listed in + `containerConfig.image.baseImage` instead of + its previous location at `containerConfig.baseImage.baseImage`. This change makes the configuration consistent with + the config.yaml file in LISA v2.0 and prior. +2. The CreateModel API `containerConfig.image` object no longer requires the `path` option. We identified that this was + a confusing and redundant option to set, considering + that the path was based on the LISA code repository structure, and that we already had an option to specify if a + model was using TGI, TEI, or vLLM. Specifying the `inferenceContainer` + is sufficient for the system to infer which files to use so that the user does not have to provide this information. +3. The ApiDeployment stack now follows the same naming convention as the rest of the stacks that we deploy, utilization + the deployment name and the deploymentStage names. This allows users + to have multiple LISA installations with different parameters in the same account without needing to change region or + account entirely. After successful deployment, you may safely delete the + previous `${deploymentStage}-LisaApiDeployment` stack, as it is no longer in use. +4. If you have installed v3.0.0 or v3.0.1, you will need to **delete** the Models API stack so that the model deployer + function will deploy again. The function was converted to a Docker Image + Function so that the growing Function size would fit within the Lambda constraints. We recommend that you take the + following actions to avoid leaked resources: + 1. Use the Model Management UI to **delete all models** from LISA. This is needed so that we delete any + CloudFormation stacks that track GPU instances. Failure to do this will require manual + resource cleanup to rid the account of inaccessible EC2 instances. Once the Models DynamoDB Table is deleted, we + do not have a programmatic way to re-reference deployed models, so that is + why we recommend deleting them first. + 2. **Only after deleting all models through the Model Management UI**, manually delete the Model Management API + stack in CloudFormation. This will take at least 45 minutes due to Lambda's use + of Elastic Network Interfaces for VPC access. The stack name will look like: + `${deployment}-lisa-models-${deploymentStage}`. + 3. After the stack has been deleted, deploy LISA v3.1.0, which will recreate the Models API stack, along with the + Docker Lambda Function. +5. The `ecsModels` section of `config.yaml` has been stripped down to only 3 fields per model: `modelName`, + `inferenceContainer`, and `baseImage`. Just as before, the system will check to see if the models + defined here exist in your models S3 bucket prior to LISA deployment. These values will be needed later when invoking + the Model Management API to create a model. diff --git a/lib/docs/user/chat.md b/lib/docs/user/chat.md index 30e82fab..35425f65 100644 --- a/lib/docs/user/chat.md +++ b/lib/docs/user/chat.md @@ -73,7 +73,8 @@ npm run dev The LISA Serve endpoint can be used independently of the Chat UI, and the following shows a few examples of how to do that. The Serve endpoint will still validate user auth, so if you have a Bearer token from the IdP configured with LISA, we will honor it, or if you've set up an API -token using the [DynamoDB instructions](#programmatic-api-tokens), we will also accept that. This diagram shows the LISA Serve components that +token using the [DynamoDB instructions](/admin/api-tokens), we will also accept that. This diagram shows the LISA Serve +components that would be utilized during direct REST API requests. ## OpenAI Specification Compatibility @@ -89,7 +90,8 @@ routes as long as your underlying models can also respond to them. - /embeddings By supporting the OpenAI spec, we can more easily allow users to integrate their collection of models into their LLM applications and workflows. In LISA, users can authenticate -using their OpenID Connect Identity Provider, or with an API token created through the DynamoDB token workflow as described [here](#programmatic-api-tokens). Once the token +using their OpenID Connect Identity Provider, or with an API token created through the DynamoDB token workflow as +described [here](/admin/api-tokens). Once the token is retrieved, users can use that in direct requests to the LISA Serve REST API. If using the IdP, users must set the 'Authorization' header, otherwise if using the API token, either the 'Api-Key' header or the 'Authorization' header. After that, requests to `https://${lisa_serve_alb}/v2/serve` will handle the OpenAI API calls. As an example, the following call can list all models that LISA is aware of, assuming usage of the API token. If you are using a self-signed cert, you must also provide the `--cacert $path` option to specify a CA bundle to trust for SSL verification. @@ -112,7 +114,8 @@ automatically be added to the base URL, just as we appended `/models` to the `/v For developers that desire an LLM assistant to help with programming tasks, we support adding LISA as an LLM provider for the [Continue plugin](https://www.continue.dev). To add LISA as a provider, open up the Continue plugin's `config.json` file and locate the `models` list. In this list, add the following block, replacing the placeholder URL -with your own REST API domain or ALB. The `/v2/serve` is required at the end of the `apiBase`. This configuration requires an API token as created through the [DynamoDB workflow](#programmatic-api-tokens). +with your own REST API domain or ALB. The `/v2/serve` is required at the end of the `apiBase`. This configuration +requires an API token as created through the [DynamoDB workflow](/admin/api-tokens). ```json { @@ -147,7 +150,7 @@ client.models.list() To use the models being served by LISA, the client needs only a few changes: 1. Specify the `base_url` as the LISA Serve ALB, using the /v2/serve route at the end, similar to the apiBase in the [Continue example](#continue-jetbrains-and-vs-code-plugin) -2. Add the API key that you generated from the [token generation steps](#programmatic-api-tokens) as your `api_key` field. +2. Add the API key that you generated from the [token generation steps](/admin/api-tokens) as your `api_key` field. 3. If using a self-signed cert, you must provide a certificate path for validating SSL. If you're using an ACM or public cert, then this may be omitted. 1. We provide a convenience function in the `lisa-sdk` for generating a cert path from an IAM certificate ARN if one is provided in the `RESTAPI_SSL_CERT_ARN` environment variable. diff --git a/lib/docs/config/hiding-chat-components.md b/lib/docs/user/hiding-chat-components.md similarity index 100% rename from lib/docs/config/hiding-chat-components.md rename to lib/docs/user/hiding-chat-components.md diff --git a/lib/docs/config/model-management-ui.md b/lib/docs/user/model-management-ui.md similarity index 100% rename from lib/docs/config/model-management-ui.md rename to lib/docs/user/model-management-ui.md diff --git a/lib/schema.ts b/lib/schema.ts index 6473edba..bcd90692 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -1,18 +1,18 @@ /** - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - Licensed 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 + Licensed 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 + 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. -*/ + 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. + */ // Models for schema validation. import * as fs from 'fs'; @@ -32,14 +32,6 @@ const REMOVAL_POLICIES: Record = { retain: cdk.RemovalPolicy.RETAIN, }; -/** - * Enum for different types of models. - */ -export enum ModelType { - TEXTGEN = 'textgen', - EMBEDDING = 'embedding', -} - /** * Enum for different types of ECS container image sources. */ @@ -50,31 +42,12 @@ export enum EcsSourceType { TARBALL = 'tarball', } -/** - * Details and configurations of a registered model. - * - * @property {string} provider - Model provider, of the form .. - * @property {string} modelName - The unique name that identifies the model. - * @property {string} modelId - The unique user-provided name for the model. - * @property {ModelType} modelType - Specifies the type of model (e.g., 'textgen', 'embedding'). - * @property {string} endpointUrl - The URL endpoint where the model can be accessed or invoked. - * @property {boolean} streaming - Indicates whether the model supports streaming capabilities. - */ -export type RegisteredModel = { - provider: string; - modelId: string; - modelName: string; - modelType: ModelType; - endpointUrl: string; - streaming?: boolean; -}; - /** * Custom security groups for application. * - * @property {ec2.SecurityGroup} ecsModelAlbSg - ECS model application load balancer security group. - * @property {ec2.SecurityGroup} restApiAlbSg - REST API application load balancer security group. - * @property {ec2.SecurityGroup} lambdaSecurityGroup - Lambda security group. + * @property {ec2.SecurityGroup} ecsModelAlbSg - .describe('ECS model application load balancer security group.') + * @property {ec2.SecurityGroup} restApiAlbSg - .describe('REST API application load balancer security group.') + * @property {ec2.SecurityGroup} lambdaSecurityGroup - .describe('Lambda security group.') */ export type SecurityGroups = { ecsModelAlbSg: ec2.SecurityGroup; @@ -82,22 +55,13 @@ export type SecurityGroups = { lambdaSecurityGroup: ec2.SecurityGroup; }; -/** - * Metadata for a specific EC2 instance type. - * - * @property {number} memory - Memory in megabytes (MB). - * @property {number} gpuCount - Number of GPUs. - * @property {string} nvmePath - Path to NVMe drive to mount. - * @property {number} maxThroughput - Maximum network throughput in gigabits per second (Gbps). - * @property {number} vCpus - Number of virtual CPUs (vCPUs). - */ const Ec2TypeSchema = z.object({ - memory: z.number(), - gpuCount: z.number().min(0), - nvmePath: z.string().optional().default(''), - maxThroughput: z.number(), - vCpus: z.number(), -}); + memory: z.number().describe('Memory in megabytes (MB)'), + gpuCount: z.number().min(0).describe('Number of GPUs'), + nvmePath: z.string().default('').describe('Path to NVMe drive to mount'), + maxThroughput: z.number().describe('Maximum network throughput in gigabits per second (Gbps)'), + vCpus: z.number().describe('Number of virtual CPUs (vCPUs)'), +}).describe('Metadata for a specific EC2 instance type.'); type Ec2Type = z.infer; @@ -258,12 +222,12 @@ export class Ec2Metadata { }; /** - * Getter method to access EC2 metadata. Retrieves the metadata for a specific EC2 instance type. - * - * @param {string} key - The key representing the EC2 instance type (e.g., 'g4dn.xlarge'). - * @throws {Error} Throws an error if no metadata is found for the specified EC2 instance type. - * @returns {Ec2Type} The metadata for the specified EC2 instance type. - */ + * Getter method to access EC2 metadata. Retrieves the metadata for a specific EC2 instance type. + * + * @param {string} key - .describe('The key representing the EC2 instance type (e.g., 'g4dn.xlarge').') + * @throws {Error} Throws an error if no metadata is found for the specified EC2 instance type. + * @returns {Ec2Type} The metadata for the specified EC2 instance type. + */ static get (key: string): Ec2Type { const instance = this.instances[key]; if (!instance) { @@ -273,10 +237,10 @@ export class Ec2Metadata { } /** - * Get EC2 instances defined with metadata. - * - * @returns {string[]} Array of EC2 instances. - */ + * Get EC2 instances defined with metadata. + * + * @returns {string[]} Array of EC2 instances. + */ static getValidInstanceKeys (): string[] { return Object.keys(this.instances); } @@ -284,68 +248,43 @@ export class Ec2Metadata { const VALID_INSTANCE_KEYS = Ec2Metadata.getValidInstanceKeys() as [string, ...string[]]; -/** - * Configuration for container health checks. - * - * @property {string[]} [command=['CMD-SHELL', 'exit 0']] - The command to run for health checks. - * @property {number} [interval=10] - The time interval between health checks, in seconds. - * @property {number} [startPeriod=30] - The time to wait before starting the first health check, in seconds. - * @property {number} [timeout=5] - The maximum time allowed for each health check to complete, in seconds. - * @property {number} [retries=2] - The number of times to retry a failed health check before considering the container - * as unhealthy. - */ const ContainerHealthCheckConfigSchema = z.object({ - command: z.array(z.string()).default(['CMD-SHELL', 'exit 0']), - interval: z.number().default(10), - startPeriod: z.number().default(30), - timeout: z.number().default(5), - retries: z.number().default(2), -}); + command: z.array(z.string()).default(['CMD-SHELL', 'exit 0']).describe('The command to run for health checks'), + interval: z.number().default(10).describe('The time interval between health checks, in seconds.'), + startPeriod: z.number().default(30).describe('The time to wait before starting the first health check, in seconds.'), + timeout: z.number().default(5).describe('The maximum time allowed for each health check to complete, in seconds'), + retries: z.number().default(2).describe('The number of times to retry a failed health check before considering the container as unhealthy.'), +}) + .describe('Configuration for container health checks'); -/** - * Container image that will use tarball on disk - */ const ImageTarballAsset = z.object({ path: z.string(), type: z.literal(EcsSourceType.TARBALL), -}); +}) + .describe('Container image that will use tarball on disk'); -/** - * Container image that will be built based on Dockerfile and assets at the supplied path - */ const ImageSourceAsset = z.object({ baseImage: z.string(), path: z.string(), type: z.literal(EcsSourceType.ASSET), -}); +}) + .describe('Container image that will be built based on Dockerfile and assets at the supplied path'); -/** - * Container image that will be pulled from the specified ECR repository - */ const ImageECRAsset = z.object({ repositoryArn: z.string(), tag: z.string().optional(), type: z.literal(EcsSourceType.ECR), -}); +}) + .describe('Container image that will be pulled from the specified ECR repository'); -/** - * Container image that will be pulled from the specified public registry - */ const ImageRegistryAsset = z.object({ registry: z.string(), type: z.literal(EcsSourceType.REGISTRY), -}); +}) + .describe('Container image that will be pulled from the specified public registry'); -/** - * Configuration for a container. - * - * @property {string} baseImage - Base image for the container. - * @property {Record} [environment={}] - Environment variables for the container. - * @property {ContainerHealthCheckConfig} [healthCheckConfig={}] - Health check configuration for the container. - * @property {number} [sharedMemorySize=0] - The value for the size of the /dev/shm volume. - */ const ContainerConfigSchema = z.object({ - image: z.union([ImageTarballAsset, ImageSourceAsset, ImageECRAsset, ImageRegistryAsset]), + image: z.union([ImageTarballAsset, ImageSourceAsset, ImageECRAsset, ImageRegistryAsset]).describe('Base image for the container.'), environment: z .record(z.any()) .transform((obj) => { @@ -357,109 +296,62 @@ const ContainerConfigSchema = z.object({ {} as Record, ); }) - .default({}), - sharedMemorySize: z.number().min(0).optional().default(0), + .default({}) + .describe('Environment variables for the container.'), + sharedMemorySize: z.number().min(0).default(0).describe('The value for the size of the /dev/shm volume.'), healthCheckConfig: ContainerHealthCheckConfigSchema.default({}), -}); +}).describe('Configuration for the container.'); -/** - * Configuration schema for health checks in load balancer settings. - * - * @property {string} path - Path for the health check. - * @property {number} [interval=30] - Interval in seconds between health checks. - * @property {number} [timeout=10] - Timeout in seconds for each health check. - * @property {number} [healthyThresholdCount=2] - Number of consecutive successful health checks required to consider - * the target healthy. - * @property {number} [unhealthyThresholdCount=2] - Number of consecutive failed health checks required to consider the - * target unhealthy. - */ const HealthCheckConfigSchema = z.object({ - path: z.string(), - interval: z.number().default(30), - timeout: z.number().default(10), - healthyThresholdCount: z.number().default(2), - unhealthyThresholdCount: z.number().default(2), -}); + path: z.string().describe('Path for the health check.'), + interval: z.number().default(30).describe('Interval in seconds between health checks.'), + timeout: z.number().default(10).describe('Timeout in seconds for each health check.'), + healthyThresholdCount: z.number().default(2).describe('Number of consecutive successful health checks required to consider the target healthy.'), + unhealthyThresholdCount: z.number().default(2).describe('Number of consecutive failed health checks required to consider the target unhealthy.'), +}) + .describe('Health check configuration for the load balancer.'); -/** - * Configuration schema for the load balancer. - * - * @property {string} [sslCertIamArn=null] - SSL certificate IAM ARN for load balancer. - * @property {HealthCheckConfig} healthCheckConfig - Health check configuration for the load balancer. - * @property {string} domainName - Domain name to use instead of the load balancer's default DNS name. - */ const LoadBalancerConfigSchema = z.object({ - sslCertIamArn: z.string().optional().nullable().default(null), + sslCertIamArn: z.string().nullish().default(null).describe('SSL certificate IAM ARN for load balancer.'), healthCheckConfig: HealthCheckConfigSchema, - domainName: z.string().optional().nullable().default(null), -}); + domainName: z.string().nullish().default(null).describe('Domain name to use instead of the load balancer\'s default DNS name.'), +}) + .describe('Configuration for load balancer settings.'); -/** - * Configuration schema for ECS auto scaling metrics. - * - * @property {string} AlbMetricName - Name of the ALB metric. - * @property {number} targetValue - Target value for the metric. - * @property {number} [duration=60] - Duration in seconds for metric evaluation. - * @property {number} [estimatedInstanceWarmup=180] - Estimated warm-up time in seconds until a newly launched instance - * can send metrics to CloudWatch. - * - */ const MetricConfigSchema = z.object({ - AlbMetricName: z.string(), - targetValue: z.number(), - duration: z.number().default(60), - estimatedInstanceWarmup: z.number().min(0).default(180), -}); + AlbMetricName: z.string().describe('Name of the ALB metric.'), + targetValue: z.number().describe('Target value for the metric.'), + duration: z.number().default(60).describe('Duration in seconds for metric evaluation.'), + estimatedInstanceWarmup: z.number().min(0).default(180).describe('Estimated warm-up time in seconds until a newly launched instance can send metrics to CloudWatch.'), +}) + .describe('Metric configuration for ECS auto scaling.'); -/** - * Configuration schema for ECS auto scaling settings. -* -* @property {number} [minCapacity=1] - Minimum capacity for auto scaling. Must be at least 1. -* @property {number} [maxCapacity=2] - Maximum capacity for auto scaling. Must be at least 1. -* @property {number} [cooldown=420] - Cool down period in seconds between scaling activities. -* @property {number} [defaultInstanceWarmup=180] - Default warm-up time in seconds until a newly launched instance can - send metrics to CloudWatch. -* @property {MetricConfig} metricConfig - Metric configuration for auto scaling. -*/ const AutoScalingConfigSchema = z.object({ blockDeviceVolumeSize: z.number().min(30).default(30), - minCapacity: z.number().min(1).default(1), - maxCapacity: z.number().min(1).default(2), - defaultInstanceWarmup: z.number().default(180), - cooldown: z.number().min(1).default(420), + minCapacity: z.number().min(1).default(1).describe('Minimum capacity for auto scaling. Must be at least 1.'), + maxCapacity: z.number().min(1).default(2).describe('Maximum capacity for auto scaling. Must be at least 1.'), + defaultInstanceWarmup: z.number().default(180).describe('Default warm-up time in seconds until a newly launched instance can'), + cooldown: z.number().min(1).default(420).describe('Cool down period in seconds between scaling activities.'), metricConfig: MetricConfigSchema, -}); +}) + .describe('Configuration for auto scaling settings.'); -/** - * Configuration schema for an ECS model. - * - * @property {AmiHardwareType} amiHardwareType - Name of the model. - * @property {AutoScalingConfigSchema} autoScalingConfig - Configuration for auto scaling settings. - * @property {Record} buildArgs - Optional build args to be applied when creating the - * task container if containerConfig.image.type is ASSET - * @property {ContainerConfig} containerConfig - Configuration for the container. - * @property {number} [containerMemoryBuffer=2048] - This is the amount of memory to buffer (or subtract off) - * from the total instance memory, if we don't include this, - * the container can have a hard time finding available RAM - * resources to start and the tasks will fail deployment - * @property {Record} environment - Environment variables set on the task container - * @property {identifier} modelType - Unique identifier for the cluster which will be used when naming resources - * @property {string} instanceType - EC2 instance type for running the model. - * @property {boolean} [internetFacing=false] - Whether or not the cluster will be configured as internet facing - * @property {LoadBalancerConfig} loadBalancerConfig - Configuration for load balancer settings. - */ const EcsBaseConfigSchema = z.object({ - amiHardwareType: z.nativeEnum(AmiHardwareType), - autoScalingConfig: AutoScalingConfigSchema, - buildArgs: z.record(z.string()).optional(), + amiHardwareType: z.nativeEnum(AmiHardwareType).describe('Name of the model.'), + autoScalingConfig: AutoScalingConfigSchema.describe('Configuration for auto scaling settings.'), + buildArgs: z.record(z.string()).optional() + .describe('Optional build args to be applied when creating the task container if containerConfig.image.type is ASSET'), containerConfig: ContainerConfigSchema, - containerMemoryBuffer: z.number().default(1024 * 2), - environment: z.record(z.string()), + containerMemoryBuffer: z.number().default(1024 * 2) + .describe('This is the amount of memory to buffer (or subtract off) from the total instance memory, ' + + 'if we don\'t include this, the container can have a hard time finding available RAM resources to start and the tasks will fail deployment'), + environment: z.record(z.string()).describe('Environment variables set on the task container'), identifier: z.string(), - instanceType: z.enum(VALID_INSTANCE_KEYS), - internetFacing: z.boolean().default(false), + instanceType: z.enum(VALID_INSTANCE_KEYS).describe('EC2 instance type for running the model.'), + internetFacing: z.boolean().default(false).describe('Whether or not the cluster will be configured as internet facing'), loadBalancerConfig: LoadBalancerConfigSchema, -}); +}) + .describe('Configuration schema for an ECS model'); /** * Type representing configuration for an ECS model. @@ -471,23 +363,18 @@ type EcsBaseConfig = z.infer; */ export type ECSConfig = EcsBaseConfig; -/** - * Configuration schema for an ECS model. - * - * @property {string} modelName - Name of the model. - * @property {string} baseImage - Base image for the container. - * @property {string} inferenceContainer - Prebuilt inference container for serving model. - */ const EcsModelConfigSchema = z .object({ - modelName: z.string(), - baseImage: z.string(), + modelName: z.string().describe('Name of the model.'), + baseImage: z.string().describe('Base image for the container.'), inferenceContainer: z .union([z.literal('tgi'), z.literal('tei'), z.literal('instructor'), z.literal('vllm')]) .refine((data) => { return !data.includes('.'); // string cannot contain a period }) - }); + .describe('Prebuilt inference container for serving model.'), + }) + .describe('Configuration schema for an ECS model.'); /** * Type representing configuration for an ECS model. @@ -499,15 +386,6 @@ type EcsModelConfig = z.infer; */ export type ModelConfig = EcsModelConfig; -/** - * Configuration schema for authorization. - * - * @property {string} [authority=null] - URL of OIDC authority. - * @property {string} [clientId=null] - Client ID for OIDC IDP . - * @property {string} [adminGroup=null] - Name of the admin group. - * @property {string} [jwtGroupsProperty=null] - Name of the JWT groups property. - * @property {string[]} [additionalScopes=null] - Additional JWT scopes to request. - */ const AuthConfigSchema = z.object({ authority: z.string().transform((value) => { if (value.endsWith('/')) { @@ -515,48 +393,32 @@ const AuthConfigSchema = z.object({ } else { return value; } - }), - clientId: z.string(), - adminGroup: z.string().optional().default(''), - jwtGroupsProperty: z.string().optional().default(''), - additionalScopes: z.array(z.string()).optional().default([]), -}); + }) + .describe('URL of OIDC authority.'), + clientId: z.string().describe('Client ID for OIDC IDP .'), + adminGroup: z.string().default('').describe('Name of the admin group.'), + jwtGroupsProperty: z.string().default('').describe('Name of the JWT groups property.'), + additionalScopes: z.array(z.string()).default([]).describe('Additional JWT scopes to request.'), +}).describe('Configuration schema for authorization.'); -/** - * Configuration schema for RDS Instances needed for LiteLLM scaling or PGVector RAG operations. - * - * The optional fields can be omitted to create a new database instance, otherwise fill in all fields to use - * an existing database instance. - * - * @property {string} username - Database username. - * @property {string} passwordSecretId - SecretsManager Secret ID that stores an existing database password. - * @property {string} dbHost - Database hostname for existing database instance. - * @property {string} dbName - Database name for existing database instance. - * @property {number} dbPort - Port to open on the database instance. - */ const RdsInstanceConfig = z.object({ - username: z.string().optional().default('postgres'), - passwordSecretId: z.string().optional(), - dbHost: z.string().optional(), - dbName: z.string().optional().default('postgres'), - dbPort: z.number().optional().default(5432), -}); + username: z.string().default('postgres').describe('Database username.'), + passwordSecretId: z.string().optional().describe('SecretsManager Secret ID that stores an existing database password.'), + dbHost: z.string().optional().describe('Database hostname for existing database instance.'), + dbName: z.string().default('postgres').describe('Database name for existing database instance.'), + dbPort: z.number().default(5432).describe('Port to open on the database instance.'), +}).describe('Configuration schema for RDS Instances needed for LiteLLM scaling or PGVector RAG operations.\n \n ' + + 'The optional fields can be omitted to create a new database instance, otherwise fill in all fields to use an existing database instance.'); -/** - * Configuration schema for REST API. - * - * @property {boolean} [internetFacing=true] - Whether the REST API ALB will be configured as internet facing. - * @property {string} sslCertIamArn - ARN of the self-signed cert to be used throughout the system - */ const FastApiContainerConfigSchema = z.object({ - internetFacing: z.boolean().default(true), - domainName: z.string().optional().nullable().default(null), - sslCertIamArn: z.string().optional().nullable().default(null), - rdsConfig: RdsInstanceConfig.optional() + internetFacing: z.boolean().default(true).describe('Whether the REST API ALB will be configured as internet facing.'), + domainName: z.string().nullish().default(null), + sslCertIamArn: z.string().nullish().default(null).describe('ARN of the self-signed cert to be used throughout the system'), + rdsConfig: RdsInstanceConfig .default({ dbName: 'postgres', username: 'postgres', - dbPort: 5432 + dbPort: 5432, }) .refine( (config) => { @@ -564,11 +426,11 @@ const FastApiContainerConfigSchema = z.object({ }, { message: - 'We do not allow using an existing DB for LiteLLM because of its requirement in internal model management ' + - 'APIs. Please do not define the dbHost or passwordSecretId fields for the FastAPI container DB config.', + 'We do not allow using an existing DB for LiteLLM because of its requirement in internal model management ' + + 'APIs. Please do not define the dbHost or passwordSecretId fields for the FastAPI container DB config.', }, ), -}); +}).describe('Configuration schema for REST API.'); /** * Enum for different types of RAG repositories available @@ -591,9 +453,6 @@ const OpenSearchExistingClusterConfig = z.object({ endpoint: z.string(), }); -/** - * Configuration schema for RAG repository. Defines settings for OpenSearch. - */ const RagRepositoryConfigSchema = z .object({ repositoryId: z.string(), @@ -602,33 +461,22 @@ const RagRepositoryConfigSchema = z rdsConfig: RdsInstanceConfig.optional(), }) .refine((input) => { - if ( - (input.type === RagRepositoryType.OPENSEARCH && input.opensearchConfig === undefined) || - (input.type === RagRepositoryType.PGVECTOR && input.rdsConfig === undefined) - ) { - return false; - } - return true; - }); + return !((input.type === RagRepositoryType.OPENSEARCH && input.opensearchConfig === undefined) || + (input.type === RagRepositoryType.PGVECTOR && input.rdsConfig === undefined)); + }) + .describe('Configuration schema for RAG repository. Defines settings for OpenSearch.'); -/** - * Configuration schema for RAG file processing. Determines the chunk size and chunk overlap when processing documents. - */ const RagFileProcessingConfigSchema = z.object({ chunkSize: z.number().min(100).max(10000), chunkOverlap: z.number().min(0), -}); +}) + .describe('Configuration schema for RAG file processing. Determines the chunk size and chunk overlap when processing documents.'); -/** - * Configuration schema for pypi. - * - * @property {string} [indexUrl=''] - URL for the pypi index. - * @property {string} [trustedHost=''] - Trusted host for pypi. - */ const PypiConfigSchema = z.object({ - indexUrl: z.string().optional().default(''), - trustedHost: z.string().optional().default(''), -}); + indexUrl: z.string().default('').describe('URL for the pypi index.'), + trustedHost: z.string().default('').describe('Trusted host for pypi.'), +}) + .describe('Configuration schema for pypi'); /** * Enum for different types of stack synthesizers @@ -639,116 +487,83 @@ export enum stackSynthesizerType { LegacyStackSynthesizer = 'LegacyStackSynthesizer', } -/** - * Configuration schema for API Gateway Endpoint - * - * @property {string} domainName - Custom domain name for API Gateway Endpoint - */ const ApiGatewayConfigSchema = z .object({ - domainName: z.string().optional().nullable().default(null), + domainName: z.string().nullish().default(null).describe('Custom domain name for API Gateway Endpoint'), }) - .optional(); + .optional() + .describe('Configuration schema for API Gateway Endpoint'); -/** - * Core LiteLLM configuration. - * See https://litellm.vercel.app/docs/proxy/configs#all-settings for more details about each field. - */ const LiteLLMConfig = z.object({ db_key: z.string().refine( (key) => key.startsWith('sk-'), // key needed for model management actions 'Key string must be defined for model management operations, and it must start with "sk-".' + - 'This can be any string, and a random UUID is recommended. Example: sk-f132c7cc-059c-481b-b5ca-a42e191672aa', + 'This can be any string, and a random UUID is recommended. Example: sk-f132c7cc-059c-481b-b5ca-a42e191672aa', ), -}); +}) + .describe('Core LiteLLM configuration - see https://litellm.vercel.app/docs/proxy/configs#all-settings for more details about each field.'); -/** - * Raw application configuration schema. - * - * @property {string} [appName='lisa'] - Name of the application. - * @property {string} [profile=null] - AWS CLI profile for deployment. - * @property {string} deploymentName - Name of the deployment. - * @property {string} accountNumber - AWS account number for deployment. Must be 12 digits. - * @property {string} region - AWS region for deployment. - * @property {string} deploymentStage - Deployment stage for the application. - * @property {string} removalPolicy - Removal policy for resources (destroy or retain). - * @property {boolean} [runCdkNag=false] - Whether to run CDK Nag checks. - * @property {string} [lambdaSourcePath='./lambda'] - Path to Lambda source code dir. - * @property {string} s3BucketModels - S3 bucket for models. - * @property {string} mountS3DebUrl - URL for S3-mounted Debian package. - * @property {string[]} [accountNumbersEcr=null] - List of AWS account numbers for ECR repositories. - * @property {boolean} [deployRag=false] - Whether to deploy RAG stacks. - * @property {boolean} [deployChat=true] - Whether to deploy chat stacks. - * @property {boolean} [deployDocs=true] - Whether to deploy docs stacks. - * @property {boolean} [deployUi=true] - Whether to deploy UI stacks. - * @property {string} logLevel - Log level for application. - * @property {AuthConfigSchema} authConfig - Authorization configuration. - * @property {RagRepositoryConfigSchema} ragRepositoryConfig - Rag Repository configuration. - * @property {RagFileProcessingConfigSchema} ragFileProcessingConfig - Rag file processing configuration. - * @property {EcsModelConfigSchema[]} ecsModels - Array of ECS model configurations. - * @property {ApiGatewayConfigSchema} apiGatewayConfig - API Gateway Endpoint configuration. - * @property {string} [nvmeHostMountPath='/nvme'] - Host path for NVMe drives. - * @property {string} [nvmeContainerMountPath='/nvme'] - Container path for NVMe drives. - * @property {Array<{ Key: string, Value: string }>} [tags=null] - Array of key-value pairs for tagging. - * @property {string} [deploymentPrefix=null] - Prefix for deployment resources. - * @property {string} [webAppAssetsPath=null] - Optional path to precompiled webapp assets. If not - * specified the web application will be built at deploy - * time. - */ const RawConfigSchema = z .object({ - appName: z.string().default('lisa'), + appName: z.string().default('lisa').describe('Name of the application.'), profile: z .string() - .optional() - .nullable() - .transform((value) => value ?? ''), - deploymentName: z.string().default('prod'), + .nullish() + .transform((value) => value ?? '') + .describe('AWS CLI profile for deployment.'), + deploymentName: z.string().default('prod').describe('Name of the deployment.'), accountNumber: z .number() .or(z.string()) .transform((value) => value.toString()) .refine((value) => value.length === 12, { message: 'AWS account number should be 12 digits. If your account ID starts with 0, then please surround the ID with quotation marks.', - }), - region: z.string(), + }) + .describe('AWS account number for deployment. Must be 12 digits.'), + region: z.string().describe('AWS region for deployment.'), restApiConfig: FastApiContainerConfigSchema, - vpcId: z.string().optional(), + vpcId: z.string().optional().describe('VPC ID for the application. (e.g. vpc-0123456789abcdef)'), subnets: z.array(z.object({ subnetId: z.string().startsWith('subnet-'), ipv4CidrBlock: z.string() - })).optional(), - deploymentStage: z.string().default('prod'), - removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]).transform((value) => REMOVAL_POLICIES[value]).default('destroy'), - runCdkNag: z.boolean().default(false), - privateEndpoints: z.boolean().optional().default(false), - s3BucketModels: z.string(), - mountS3DebUrl: z.string().optional(), + })).optional().describe('Array of subnet objects for the application. These contain a subnetId(e.g. [subnet-fedcba9876543210] and ipv4CidrBlock'), + deploymentStage: z.string().default('prod').describe('Deployment stage for the application.'), + removalPolicy: z.union([z.literal('destroy'), z.literal('retain')]) + .transform((value) => REMOVAL_POLICIES[value]) + .default('destroy') + .describe('Removal policy for resources (destroy or retain).'), + runCdkNag: z.boolean().default(false).describe('Whether to run CDK Nag checks.'), + privateEndpoints: z.boolean().default(false).describe('Whether to use privateEndpoints for REST API.'), + s3BucketModels: z.string().describe('S3 bucket for models.'), + mountS3DebUrl: z.string().describe('URL for S3-mounted Debian package.'), accountNumbersEcr: z .array(z.union([z.number(), z.string()])) .transform((arr) => arr.map(String)) .refine((value) => value.every((num) => num.length === 12), { message: 'AWS account number should be 12 digits. If your account ID starts with 0, then please surround the ID with quotation marks.', }) - .optional(), - deployRag: z.boolean().optional().default(true), - deployChat: z.boolean().optional().default(true), - deployDocs: z.boolean().optional().default(true), - deployUi: z.boolean().optional().default(true), - logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]).default('DEBUG'), - authConfig: AuthConfigSchema.optional(), - pypiConfig: PypiConfigSchema.optional().default({ + .optional() + .describe('List of AWS account numbers for ECR repositories.'), + deployRag: z.boolean().default(true).describe('Whether to deploy RAG stacks.'), + deployChat: z.boolean().default(true).describe('Whether to deploy chat stacks.'), + deployDocs: z.boolean().default(true).describe('Whether to deploy docs stacks.'), + deployUi: z.boolean().default(true).describe('Whether to deploy UI stacks.'), + logLevel: z.union([z.literal('DEBUG'), z.literal('INFO'), z.literal('WARNING'), z.literal('ERROR')]) + .default('DEBUG') + .describe('Log level for application.'), + authConfig: AuthConfigSchema.optional().describe('Authorization configuration.'), + pypiConfig: PypiConfigSchema.default({ indexUrl: '', trustedHost: '', - }), - condaUrl: z.string().optional().default(''), - certificateAuthorityBundle: z.string().optional().default(''), - ragRepositories: z.array(RagRepositoryConfigSchema).default([]), - ragFileProcessingConfig: RagFileProcessingConfigSchema.optional(), - ecsModels: z.array(EcsModelConfigSchema).optional(), - apiGatewayConfig: ApiGatewayConfigSchema.optional(), - nvmeHostMountPath: z.string().default('/nvme'), - nvmeContainerMountPath: z.string().default('/nvme'), + }).describe('Pypi configuration.'), + condaUrl: z.string().default('').describe('Conda URL configuration'), + certificateAuthorityBundle: z.string().default('').describe('Certificate Authority Bundle file'), + ragRepositories: z.array(RagRepositoryConfigSchema).default([]).describe('Rag Repository configuration.'), + ragFileProcessingConfig: RagFileProcessingConfigSchema.optional().describe('Rag file processing configuration.'), + ecsModels: z.array(EcsModelConfigSchema).optional().describe('Array of ECS model configurations.'), + apiGatewayConfig: ApiGatewayConfigSchema, + nvmeHostMountPath: z.string().default('/nvme').describe('Host path for NVMe drives.'), + nvmeContainerMountPath: z.string().default('/nvme').describe('Container path for NVMe drives.'), tags: z .array( z.object({ @@ -756,18 +571,20 @@ const RawConfigSchema = z Value: z.string(), }), ) - .optional(), - deploymentPrefix: z.string().optional(), - webAppAssetsPath: z.string().optional(), + .optional() + .describe('Array of key-value pairs for tagging.'), + deploymentPrefix: z.string().optional().describe('Prefix for deployment resources.'), + webAppAssetsPath: z.string().optional().describe('Optional path to precompiled webapp assets. If not specified the web application will be built at deploy time.'), lambdaLayerAssets: z .object({ - authorizerLayerPath: z.string().optional(), - commonLayerPath: z.string().optional(), - fastapiLayerPath: z.string().optional(), - ragLayerPath: z.string().optional(), - sdkLayerPath: z.string().optional(), + authorizerLayerPath: z.string().optional().describe('Lambda Authorizer code path'), + commonLayerPath: z.string().optional().describe('Lambda common layer code path'), + fastapiLayerPath: z.string().optional().describe('Lambda API code path'), + ragLayerPath: z.string().optional().describe('Lambda RAG layer code path'), + sdkLayerPath: z.string().optional().describe('Lambda SDK layer code path'), }) - .optional(), + .optional() + .describe('Configuration for local Lambda layer code'), permissionsBoundaryAspect: z .object({ permissionsBoundaryPolicyName: z.string(), @@ -775,8 +592,9 @@ const RawConfigSchema = z policyPrefix: z.string().max(20).optional(), instanceProfilePrefix: z.string().optional(), }) - .optional(), - stackSynthesizer: z.nativeEnum(stackSynthesizerType).optional(), + .optional() + .describe('Aspect CDK injector for permissions. Ref: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.PermissionsBoundary.html'), + stackSynthesizer: z.nativeEnum(stackSynthesizerType).optional().describe('Set the stack synthesize type. Ref: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackSynthesizer.html'), litellmConfig: LiteLLMConfig, }) .refine((config) => (config.pypiConfig.indexUrl && config.region.includes('iso')) || !config.region.includes('iso'), { @@ -802,21 +620,22 @@ const RawConfigSchema = z (config) => { return ( !(config.deployChat || config.deployRag || config.deployUi) || - config.authConfig + config.authConfig ); }, { message: - 'An auth config must be provided when deploying the chat, RAG, or UI stacks or when deploying an internet ' + - 'facing ALB. Check that `deployChat`, `deployRag`, `deployUi`, and `restApiConfig.internetFacing` are all ' + - 'false or that an `authConfig` is provided.', + 'An auth config must be provided when deploying the chat, RAG, or UI stacks or when deploying an internet ' + + 'facing ALB. Check that `deployChat`, `deployRag`, `deployUi`, and `restApiConfig.internetFacing` are all ' + + 'false or that an `authConfig` is provided.', }, - ); + ) + .describe('Raw application configuration schema.'); /** * Apply transformations to the raw application configuration schema. * - * @param {Object} rawConfig - The raw application configuration. + * @param {Object} rawConfig - .describe('The raw application configuration.') * @returns {Object} The transformed application configuration. */ export const ConfigSchema = RawConfigSchema.transform((rawConfig) => { @@ -862,12 +681,10 @@ export const ConfigSchema = RawConfigSchema.transform((rawConfig) => { */ export type Config = z.infer; -export type FastApiContainerConfig = z.infer; - /** * Basic properties required for a stack definition in CDK. * - * @property {Config} config - The application configuration. + * @property {Config} config - .describe('The application configuration.') */ export type BaseProps = { config: Config; diff --git a/lib/stages.ts b/lib/stages.ts index 19ffce02..e1edff73 100644 --- a/lib/stages.ts +++ b/lib/stages.ts @@ -102,7 +102,7 @@ export class LisaServeApplicationStage extends Stage { baseStackProps.synthesizer = new DefaultStackSynthesizer(); break; default: - throw Error('Unrecognized config value: "stackSyntehsizer"'); + throw Error('Unrecognized config value: "stackSynthesizer"'); } } diff --git a/lib/zod2md.config.ts b/lib/zod2md.config.ts new file mode 100644 index 00000000..84e70bbd --- /dev/null +++ b/lib/zod2md.config.ts @@ -0,0 +1,24 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed 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 type { Config } from 'zod2md'; + +export default { + title: 'LISA Configuration Schema', + entry: './lib/schema.ts', + output: './lib/docs/config/schema.md', + tsconfig: 'tsconfig.json', +} satisfies Config; diff --git a/package-lock.json b/package-lock.json index 0664a897..04c23166 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,8 @@ "lint-staged": "^15.2.10", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "~5.1.6" + "typescript": "~5.1.6", + "zod2md": "^0.1.4" } }, "node_modules/@ampproject/remapping": { @@ -1736,6 +1737,15 @@ "node": ">=4.0" } }, + "node_modules/@commander-js/extra-typings": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-12.1.0.tgz", + "integrity": "sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg==", + "dev": true, + "peerDependencies": { + "commander": "~12.1.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1758,6 +1768,374 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4895,6 +5273,21 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/bundle-require": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.2.1.tgz", + "integrity": "sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.17" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -5813,6 +6206,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -9536,6 +9967,15 @@ "node": ">=18.0.0" } }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11709,6 +12149,24 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod2md": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.1.4.tgz", + "integrity": "sha512-ZEW9TZd4M9PHB/UeZcLXIjlCbzPUESGvzEN+Ttye18quh4Afap8DYd/zpIPfw+DrVsSSWoNU40HVnfE9UcpmPw==", + "dev": true, + "dependencies": { + "@commander-js/extra-typings": "^12.0.0", + "bundle-require": "^4.0.2", + "commander": "^12.0.0", + "esbuild": "^0.19.11" + }, + "bin": { + "zod2md": "dist/bin.js" + }, + "peerDependencies": { + "zod": "^3.22.0" + } } } } diff --git a/package.json b/package.json index f90ce33e..292cf808 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "prepare": "husky install", "migrate-properties": "node ./scripts/migrate-properties.mjs", "postinstall": "(cd lib/user-interface/react && npm install) && (cd lib/docs && npm install)", - "postbuild": "(cd lib/user-interface/react && npm build) && (cd lib/docs && npm build)" + "postbuild": "(cd lib/user-interface/react && npm build) && (cd lib/docs && npm build)", + "generateSchemaDocs": "npx zod2md -c ./lib/zod2md.config.ts" }, "devDependencies": { "@aws-cdk/aws-lambda-python-alpha": "2.125.0-alpha.0", @@ -36,7 +37,8 @@ "lint-staged": "^15.2.10", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "~5.1.6" + "typescript": "~5.1.6", + "zod2md": "^0.1.4" }, "dependencies": { "aws-cdk-lib": "2.125.0", From 24a82f1d58b141652f80bc12921fc6d7ab5d39bf Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 15 Nov 2024 10:24:17 -0700 Subject: [PATCH 43/48] Deployment easement documentation --- .../workflows/docs.deploy.github-pages.yml | 3 + example_config.yaml | 234 +++++++----------- lib/docs/.vitepress/config.mts | 3 +- lib/docs/admin/getting-started.md | 80 +----- lib/docs/admin/ui-configuration.md | 32 +++ lib/docs/config/branding.md | 13 - lib/docs/config/configuration.md | 12 +- lib/docs/user/breaking-changes.md | 17 ++ lib/docs/user/hiding-chat-components.md | 1 - 9 files changed, 156 insertions(+), 239 deletions(-) create mode 100644 lib/docs/admin/ui-configuration.md delete mode 100644 lib/docs/config/branding.md delete mode 100644 lib/docs/user/hiding-chat-components.md diff --git a/.github/workflows/docs.deploy.github-pages.yml b/.github/workflows/docs.deploy.github-pages.yml index ab5a0c5b..ac442a3d 100644 --- a/.github/workflows/docs.deploy.github-pages.yml +++ b/.github/workflows/docs.deploy.github-pages.yml @@ -29,6 +29,9 @@ jobs: cache: npm - name: Setup Pages uses: actions/configure-pages@v4 + - name: Install root dependencies + run: | + npm install - name: Install dependencies working-directory: ./lib/docs run: npm install diff --git a/example_config.yaml b/example_config.yaml index 1275ab32..dd86e02f 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -1,151 +1,83 @@ -env: dev - -dev: - appName: lisa - profile: - deploymentName: - accountNumber: 012345678901 - region: us-east-1 - deploymentStage: dev - removalPolicy: destroy - runCdkNag: false - # lambdaLayerAssets: - # authorizerLayerPath: /path/to/authorizer_layer.zip - # commonLayerPath: /path/to/common_layer.zip - # fastapiLayerPath: /path/to/fastapi_layer.zip - # ragLayerPath: /path/to/rag_layer.zip - # sdkLayerPath: /path/to/sdk_layer.zip - # stackSynthesizer: CliCredentialsStackSynthesizer - # permissionsBoundaryAspect: - # permissionsBoundaryPolicyName: CustomPermissionBoundary - # rolePrefix: CustomPrefix - # policyPrefix: CustomPrefix - # instanceProfilePrefix: CustomPrefix - # vpcId: vpc-0123456789abcdef, - # subnetIds: [subnet-fedcba9876543210, subnet-0987654321fedcba], - s3BucketModels: hf-models-gaiic - # aws partition mountS3 package location - mountS3DebUrl: https://s3.amazonaws.com/mountpoint-s3-release/latest/x86_64/mount-s3.deb - # aws-iso partition mountS3 package location - # mountS3DebUrl: https://mountpoint-s3-release-us-iso-east-1.s3.us-iso-east-1.c2s.ic.gov/latest/x86_64/mount-s3.deb - # aws-iso-b partition mountS3 package location - # mountS3DebUrl: https://mountpoint-s3-release-us-isob-east-1.s3.us-isob-east-1.sc2s.sgov.gov/latest/x86_64/mount-s3.deb - accountNumbersEcr: - - 012345678901 - deployRag: true - deployChat: true - deployUi: true - privateEndpoints: false - lambdaConfig: - pythonRuntime: PYTHON_3_10 - logLevel: DEBUG - vpcAutoscalingConfig: - provisionedConcurrentExecutions: 5 - minCapacity: 1 - maxCapacity: 50 - targetValue: 0.80 - cooldown: 30 - authConfig: - authority: - clientId: - adminGroup: - jwtGroupsProperty: - logLevel: DEBUG - # NOTE: The following configuration will allow for using a custom domain for the chat user interface. - # If this option is specified, the API Gateway invocation URL will NOT work on its own as the application URL. - # Users must use the custom domain for the user interface to work if this option is populated. - apiGatewayConfig: - domainName: - restApiConfig: - apiVersion: v2 - instanceType: m5.large - containerConfig: - image: - baseImage: python:3.9 - path: lib/serve/rest-api - type: asset - healthCheckConfig: - command: ["CMD-SHELL", "exit 0"] - interval: 10 - startPeriod: 30 - timeout: 5 - retries: 3 - autoScalingConfig: - minCapacity: 1 - maxCapacity: 1 - cooldown: 60 - defaultInstanceWarmup: 60 - metricConfig: - AlbMetricName: RequestCountPerTarget - targetValue: 1000 - duration: 60 - estimatedInstanceWarmup: 30 - internetFacing: true - loadBalancerConfig: - sslCertIamArn: arn:aws:iam::012345678901:server-certificate/lisa-self-signed-dev - healthCheckConfig: - path: /health - interval: 60 - timeout: 30 - healthyThresholdCount: 2 - unhealthyThresholdCount: 10 - domainName: - ragRepositories: - - repositoryId: pgvector-rag - type: pgvector - rdsConfig: - username: postgres - # - repositoryId: default - # type: opensearch - # opensearchConfig: - # dataNodes: 2 - # dataNodeInstanceType: r6g.large.search - # masterNodes: 0 - # masterNodeInstanceType: r6g.large.search - # volumeSize: 300 - # If adding an existing PGVector database, this configurations assumes: - # 1. The database has been configured to have pgvector installed and enabled: https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-rds-postgresql-pgvector-ml-model-integration/ - # 2. The database is accessible by RAG-related lambda functions (add inbound PostgreSQL access on the database's security group for all Lambda RAG security groups) - # 3. A secret ID exists in SecretsManager holding the database password within a json block of '{"password":"your_password_here"}'. This is the same format that RDS natively provides a password in SecretsManager. - # If the passwordSecretId or dbHost are not provided, then a sample database will be created for you. Only the username is required. - # - repositoryId: pgvector-rag - # type: pgvector - # rdsConfig: - # username: postgres - # passwordSecretId: # password ID as stored in SecretsManager. Example: "rds!db-aa88493d-be8d-4a3f-96dc-c668165f7826" - # dbHost: # Host name of database. Example hostname from RDS: "my-db-name.291b2f03.us-east-1.rds.amazonaws.com" - # dbName: postgres - ragFileProcessingConfig: - chunkSize: 512 - chunkOverlap: 51 - ecsModels: - - modelName: mistralai/Mistral-7B-Instruct-v0.2 - inferenceContainer: tgi - baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 - - modelName: intfloat/e5-large-v2 - inferenceContainer: tei - baseImage: ghcr.io/huggingface/text-embeddings-inference:1.2.3 - # - modelName: mistralai/Mixtral-8x7B-Instruct-v0.1 - # inferenceContainer: tgi - # baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 - # LiteLLM Config options found here: https://litellm.vercel.app/docs/proxy/configs#all-settings - # Anything within this config is copied to a configuration for starting LiteLLM in the REST API container. - # It is suggested to put an "ignored" API key so that calls to locally hosted models don't fail on OpenAI calls - # from LiteLLM. - # We added `lisa_params` to add additional metadata for interaction with the Chat UI. Specify if the model is a - # textgen or embedding model, and if it is textgen, specify whether it supports streaming. If embedding, then - # omit the `streaming` parameter. When defining the model list, the `lisa_params` will be an object in the model - # definition that will have the `model_type` and `streaming` fields in it. A commented example is provided below. - litellmConfig: - litellm_settings: - telemetry: false # Don't try to send telemetry to LiteLLM servers. - general_settings: - master_key: sk-d7a77bcb-3e23-483c-beec-2700f2baeeb1 # A key is required for model management purposes - model_list: # Add any of your existing (not LISA-hosted) models here. -# - model_name: mymodel -# litellm_params: -# model: openai/myprovider/mymodel -# api_key: ignored -# lisa_params: -# model_type: textgen -# streaming: true +accountNumber: "012345678901" +region: us-east-1 +authConfig: + authority: + clientId: + adminGroup: + jwtGroupsProperty: +s3BucketModels: hf-models-gaiic +########################### OPTIONAL BELOW ####################################### +# profile: AWS CLI profile for deployment. +# vpcId: VPC ID for the application. (e.g. vpc-0123456789abcdef) +# The following is an array of subnet objects for the application. These contain a subnetId(e.g. [subnet-fedcba9876543210] and ipv4CidrBlock +# subnets: +# - subnetId: +# ipv4CidrBlock: +# The following configuration will allow for using a custom domain for the chat user interface. +# If this option is specified, the API Gateway invocation URL will NOT work on its own as the application URL. +# Users must use the custom domain for the user interface to work if this option is populated. +# apiGatewayConfig: +# domainName: +# restApiConfig: +# sslCertIamArn: ARN of the self-signed cert to be used throughout the system +# Some customers will want to download required libs prior to deployment, provide a path to the zipped resources +# lambdaLayerAssets: +# authorizerLayerPath: /path/to/authorizer_layer.zip +# commonLayerPath: /path/to/common_layer.zip +# fastapiLayerPath: /path/to/fastapi_layer.zip +# ragLayerPath: /path/to/rag_layer.zip +# sdkLayerPath: /path/to/sdk_layer.zip +# stackSynthesizer: CliCredentialsStackSynthesizer +# deploymentPrefix: Prefix for deployment resources. +# webAppAssetsPath: Optional path to precompiled webapp assets. If not specified the web application will be built at deploy time. +# permissionsBoundaryAspect: +# permissionsBoundaryPolicyName: CustomPermissionBoundary +# rolePrefix: CustomPrefix +# policyPrefix: CustomPrefix +# instanceProfilePrefix: CustomPrefix +# vpcId: vpc-0123456789abcdef, +# aws-iso partition mountS3 package location +# mountS3DebUrl: https://mountpoint-s3-release-us-iso-east-1.s3.us-iso-east-1.c2s.ic.gov/latest/x86_64/mount-s3.deb +# aws-iso-b partition mountS3 package location +# mountS3DebUrl: https://mountpoint-s3-release-us-isob-east-1.s3.us-isob-east-1.sc2s.sgov.gov/latest/x86_64/mount-s3.deb +# List of AWS account numbers for ECR repositories. +# accountNumbersEcr: +# - 012345678901 +# ragRepositories: +# - repositoryId: pgvector-rag +# type: pgvector +# rdsConfig: +# username: postgres +# - repositoryId: default +# type: opensearch +# opensearchConfig: +# dataNodes: 2 +# dataNodeInstanceType: r6g.large.search +# masterNodes: 0 +# masterNodeInstanceType: r6g.large.search +# volumeSize: 300 +# If adding an existing PGVector database, this configurations assumes: +# 1. The database has been configured to have pgvector installed and enabled: https://aws.amazon.com/about-aws/whats-new/2023/05/amazon-rds-postgresql-pgvector-ml-model-integration/ +# 2. The database is accessible by RAG-related lambda functions (add inbound PostgreSQL access on the database's security group for all Lambda RAG security groups) +# 3. A secret ID exists in SecretsManager holding the database password within a json block of '{"password":"your_password_here"}'. This is the same format that RDS natively provides a password in SecretsManager. +# If the passwordSecretId or dbHost are not provided, then a sample database will be created for you. Only the username is required. +# - repositoryId: pgvector-rag +# type: pgvector +# rdsConfig: +# username: postgres +# passwordSecretId: # password ID as stored in SecretsManager. Example: "rds!db-aa88493d-be8d-4a3f-96dc-c668165f7826" +# dbHost: # Host name of database. Example hostname from RDS: "my-db-name.291b2f03.us-east-1.rds.amazonaws.com" +# dbName: postgres +# You can optionally provide a list of models and the deployment process will ensure they exist in your model bucket and try to download them if they don't exist +# ecsModels: +# - modelName: mistralai/Mistral-7B-Instruct-v0.2 +# inferenceContainer: tgi +# baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 +# - modelName: intfloat/e5-large-v2 +# inferenceContainer: tei +# baseImage: ghcr.io/huggingface/text-embeddings-inference:1.2.3 +# - modelName: mistralai/Mixtral-8x7B-Instruct-v0.1 +# inferenceContainer: tgi +# baseImage: ghcr.io/huggingface/text-generation-inference:2.0.1 +# litellmConfig: +# db_key: sk-d7a77bcb-3e23-483c-beec-2700f2baeeb1 # A key is required for model management purposes - must start with sk- diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts index ce20165a..f08bc2d2 100644 --- a/lib/docs/.vitepress/config.mts +++ b/lib/docs/.vitepress/config.mts @@ -25,6 +25,7 @@ const navLinks = [ { text: 'Getting Started', link: '/admin/getting-started' }, { text: 'Deployment', link: '/admin/deploy' }, { text: 'Model Management API Usage', link: '/admin/model-management' }, + { text: 'Chat UI Configuration', link: '/admin/ui-configuration' }, { text: 'API Request Error Handling', link: '/admin/error' }, ], }, @@ -34,7 +35,6 @@ const navLinks = [ { text: 'Configuration Schema', link: '/config/configuration' }, { text: 'Model Compatibility', link: '/config/model-compatibility' }, { text: 'Rag Vector Stores', link: '/config/vector-stores' }, - { text: 'Branding', link: '/config/branding' }, { text: 'Configure IdP: Cognito & Keycloak Examples', link: '/config/idp' }, { text: 'LiteLLM', link: '/config/lite-llm' }, ], @@ -47,7 +47,6 @@ const navLinks = [ { text: 'Context Windows', link: '/user/context-windows' }, { text: 'Model KWARGS', link: '/user/model-kwargs' }, { text: 'Model Management UI', link: '/user/model-management-ui' }, - { text: 'Hiding Advanced Chat UI Components', link: '/user/hiding-chat-components' }, { text: 'Non-RAG in Context File Management', link: '/user/nonrag-management' }, { text: 'Prompt Engineering', link: '/user/prompt-engineering' }, { text: 'Session History', link: '/user/history' }, diff --git a/lib/docs/admin/getting-started.md b/lib/docs/admin/getting-started.md index af66e3c2..df2ffe26 100644 --- a/lib/docs/admin/getting-started.md +++ b/lib/docs/admin/getting-started.md @@ -36,10 +36,10 @@ cd lisa ## Step 2: Set Up Environment Variables -Create and configure your `config.yaml` file: +Create and configure your `config-custom.yaml` file: ```bash -cp example_config.yaml config.yaml +cp example_config.yaml config-custom.yaml ``` Set the following environment variables: @@ -80,16 +80,15 @@ make installTypeScriptRequirements ## Step 4: Configure LISA -Edit the `config.yaml` file to customize your LISA deployment. Key configurations include: +Edit the `config-custom.yaml` file to customize your LISA deployment. Key configurations include: - AWS account and region settings -- Model configurations - Authentication settings -- Networking and infrastructure preferences +- Model bucket name ## Step 5: Stage Model Weights -LISA requires model weights to be staged in the S3 bucket specified in your `config.yaml` file, assuming the S3 bucket follows this structure: +LISA requires model weights to be staged in the S3 bucket specified in your `config-custom.yaml` file, assuming the S3 bucket follows this structure: ``` s3:/// @@ -108,7 +107,7 @@ s3:///mistralai/Mistral-7B-Instruct-v0.2/ ... ``` -To automatically download and stage the model weights defined by the `ecsModels` parameter in your `config.yaml`, use the following command: +To automatically download and stage the model weights defined by the `ecsModels` parameter in your `config-custom.yaml`, use the following command: ```bash make modelCheck @@ -117,7 +116,7 @@ make modelCheck This command verifies if the model's weights are already present in your S3 bucket. If not, it downloads the weights, converts them to the required format, and uploads them to your S3 bucket. Ensure adequate disk space is available for this process. > **WARNING** -> As of LISA 3.0, the `ecsModels` parameter in `config.yaml` is solely for staging model weights in your S3 bucket. +> As of LISA 3.0, the `ecsModels` parameter in `config-custom.yaml` is solely for staging model weights in your S3 bucket. > Previously, before models could be managed through the [API](/admin/model-management) or via the Model Management > section of the [Chatbot](/user/chat), this parameter also > dictated which models were deployed. @@ -130,7 +129,7 @@ This command verifies if the model's weights are already present in your S3 buck ## Step 6: Configure Identity Provider -In the `config.yaml` file, configure the `authConfig` block for authentication. LISA supports OpenID Connect (OIDC) providers such as AWS Cognito or Keycloak. Required fields include: +In the `config-custom.yaml` file, configure the `authConfig` block for authentication. LISA supports OpenID Connect (OIDC) providers such as AWS Cognito or Keycloak. Required fields include: - `authority`: URL of your identity provider - `clientId`: Client ID for your application @@ -151,15 +150,9 @@ the key. Configuration example is below. ```yaml litellmConfig: - general_settings: - master_key: sk-00000000-0000-0000-0000-000000000000 # needed for db operations, create your own key # pragma: allowlist-secret - model_list: [] + db_key: sk-00000000-0000-0000-0000-000000000000 # needed for db operations, create your own key # pragma: allowlist-secret ``` -**Note**: It is possible to add LiteLLM-only models to this configuration, but it is not recommended as the models in this configuration will not show in the -Chat or Model Management UIs. Instead, use the [Model Management UI](/user/models) to add or remove LiteLLM-only model -configurations. - ## Step 8: Set Up SSL Certificates (Development Only) **WARNING: THIS IS FOR DEV ONLY** @@ -171,17 +164,16 @@ export REGION= aws iam upload-server-certificate --server-certificate-name --certificate-body file://scripts/server.pem --private-key file://scripts/server.key ``` -Update your `config.yaml` with the certificate ARN: +Update your `config-custom.yaml` with the certificate ARN: ```yaml restApiConfig: - loadBalancerConfig: - sslCertIamArn: arn:aws:iam:::server-certificate/ + sslCertIamArn: arn:aws:iam:::server-certificate/ ``` ## Step 9: Customize Model Deployment -In the `ecsModels` section of `config.yaml`, allow our deployment process to pull the model weights for you. +In the `ecsModels` section of `config-custom.yaml`, allow our deployment process to pull the model weights for you. During the deployment process, LISA will optionally attempt to download your model weights if you specify an optional `ecsModels` array, this will only work in non ADC regions. Specifically, see the `ecsModels` section of @@ -236,51 +228,3 @@ services are in the same region as the LISA installation, LISA can use them alon LISA Model Management API, and they will not show in the Chat UI. These will only show as part of the OpenAI `/models` API. Although there is support for it, we recommend using the [Model Management API](/admin/model-management) instead of the following static configuration. - -### Example Configuration - -```yaml -dev: - litellmConfig: - litellm_settings: - telemetry: false # Disable telemetry to LiteLLM servers (recommended for VPC deployments) - drop_params: true # Ignore unrecognized parameters instead of failing - - model_list: - # 1. SageMaker Endpoint Configuration - - model_name: test-endpoint # Human-readable name, can be anything and will be used for OpenAI API calls - litellm_params: - model: sagemaker/test-endpoint # Prefix required for SageMaker Endpoints and "test-endpoint" matches Endpoint name - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: textgen - streaming: true - - # 2. Amazon Bedrock Model Configuration - - model_name: bedrock-titan-express # Human-readable name for future OpenAI API calls - litellm_params: - model: bedrock/amazon.titan-text-express-v1 # Prefix required for Bedrock Models, and exact name of Model to use - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: textgen - streaming: true - - # 3. Custom OpenAI-compatible Text Generation Model - - model_name: custom-openai-model # Used in future OpenAI-compatible calls to LiteLLM - litellm_params: - model: openai/custom-provider/textgen-model # Format: openai// - api_base: https://your-domain-here:443/v1 # Your model's base URI - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: textgen - streaming: true - - # 4. Custom OpenAI-compatible Embedding Model - - model_name: custom-openai-embedding-model # Used in future OpenAI-compatible calls to LiteLLM - litellm_params: - model: openai/modelProvider/modelName # Prefix required for OpenAI-compatible models followed by model provider and name details - api_base: https://your-domain-here:443/v1 # Your model's base URI - api_key: ignored # Provide an ignorable placeholder key to avoid LiteLLM deployment failures - lisa_params: - model_type: embedding -``` diff --git a/lib/docs/admin/ui-configuration.md b/lib/docs/admin/ui-configuration.md new file mode 100644 index 00000000..9c427086 --- /dev/null +++ b/lib/docs/admin/ui-configuration.md @@ -0,0 +1,32 @@ + +# Chat UI Configuration + +The release of LISA 3.2.0 introduces enhanced administrative controls for the Chat UI, allowing for granular customization of user interfaces. System administrators now have the ability to activate, deactivate, or configure specific components for all users through the application's configuration panel. + +The following features can be managed: + +1. Session History Management + - Activate or deactivate the option to delete session history + +2. Message Information + - Control visibility of message metadata + +3. Chat Parameters + - Configure chat Kwargs + - Customize prompt templates + - Adjust chat history buffer settings + +4. Retrieval-Augmented Generation (RAG) Settings + - Modify the number of RAG documents to be included in the retrieval process (TopK) + - Activate or deactivate RAG document uploads + +5. Contextual Document Management + - Control the ability to upload in-context documents + +6. System Banner Customization + - Toggle banner visibility + - Edit banner text + - Customize text color + - Adjust background color + +These new configuration options provide administrators with greater flexibility in tailoring the Chat UI to organizational needs, enhancing both security and user experience across the platform. diff --git a/lib/docs/config/branding.md b/lib/docs/config/branding.md deleted file mode 100644 index 47ea59d6..00000000 --- a/lib/docs/config/branding.md +++ /dev/null @@ -1,13 +0,0 @@ -# Branding - -LISA supports some branding through the deployment [Configuration](/config/configuration). Adding the following -will modify the Chat UI with a custom banner: - -```yaml -systemBanner: - text: LISA System - backgroundColor: orange - fontColor: black -``` - -These values are shared across all users. diff --git a/lib/docs/config/configuration.md b/lib/docs/config/configuration.md index 0c7ea963..520e9ab3 100644 --- a/lib/docs/config/configuration.md +++ b/lib/docs/config/configuration.md @@ -1,15 +1,19 @@ # Minimal Configuration Configurations for LISA are split into 2 configuration files, base and custom. The base configuration contains the -minimal properties required to deploy LISA. The file is located at the root of your project (./config-base.yaml) and -contains the following properties: +recommended properties that can be overridden with the custom properties file. The custom configuration should contain +the minimal properties required to deploy LISA, and any optional properties or overrides. This file should be created +at the root of your project (./config-custom.yaml) and needs to contain the following properties: ```yaml accountNumber: region: -restApiConfig: s3BucketModels: -mountS3DebUrl: +authConfig: + authority: + clientId: + adminGroup: + jwtGroupsProperty: ``` diff --git a/lib/docs/user/breaking-changes.md b/lib/docs/user/breaking-changes.md index 7b64c814..b71fc184 100644 --- a/lib/docs/user/breaking-changes.md +++ b/lib/docs/user/breaking-changes.md @@ -1,5 +1,22 @@ # Breaking Changes +## Migrating to v3.2.0 + +With the release of LISA v3.2.0, we have implemented a significant update to the configuration file schema to streamline +the deployment process. The previous single config.yaml file has been deprecated in favor of a more flexible two-file +system: config-base.yaml and config-custom.yaml. + +The config-base.yaml file now contains default properties, which can be selectively overridden using the +config-custom.yaml file. This new structure allows for greater customization while maintaining a standardized base +configuration. + +To facilitate the transition to this new configuration system, we have developed a migration utility. Users can execute +the command `npm run migrate-properties` to automatically convert their existing config.yaml file into the new +config-custom.yaml format. + +This update enhances the overall flexibility and maintainability of LISA configurations, providing a more robust +foundation for future developments and easier customization for end-users. + ## v2 to v3 Migration With the release of LISA v3.0.0, we have introduced several architectural changes that are incompatible with previous diff --git a/lib/docs/user/hiding-chat-components.md b/lib/docs/user/hiding-chat-components.md deleted file mode 100644 index 46409041..00000000 --- a/lib/docs/user/hiding-chat-components.md +++ /dev/null @@ -1 +0,0 @@ -# TODO From 76857620254bdb92fcf0cda6f472e804ba8c098d Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 15 Nov 2024 11:15:38 -0700 Subject: [PATCH 44/48] Deployment easement documentation --- README.md | 27 +++++++++-------- lib/docs/admin/architecture.md | 48 ++++++++++++++----------------- lib/docs/admin/getting-started.md | 39 +++++++++++++------------ lib/docs/admin/overview.md | 22 -------------- 4 files changed, 57 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index c7253a2a..bda31e4b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # LLM Inference Solution for Amazon Dedicated Cloud (LISA) +[![Full Documentation](https://img.shields.io/badge/Full%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/LISA/) + ## What is LISA? LISA is an infrastructure-as-code solution providing scalable, low latency access to customers’ generative LLMs and @@ -19,28 +21,28 @@ API integrations with each provider. ## Key Features -* Self Host Models: Bring your own text generation and embedding models to LISA for hosting and inference. -* Model Orchestration: Centralize and standardize configuration with 100+ models from model providers via LiteLLM, +* **Self Host Models:** Bring your own text generation and embedding models to LISA for hosting and inference. +* **Model Orchestration:** Centralize and standardize configuration with 100+ models from model providers via LiteLLM, including Amazon Bedrock models. -* Chatbot User Interface: Through the chatbot user interface, users can prompt LLMs, receive responses, modify prompt +* **Chatbot User Interface:** Through the chatbot user interface, users can prompt LLMs, receive responses, modify prompt templates, change model arguments, and manage their session history. Administrators can control available features via the configuration page. -* Retrieval-augmented generation (RAG): RAG reduces the need for fine-tuning, an expensive and time-consuming +* **Retrieval-augmented generation (RAG):** RAG reduces the need for fine-tuning, an expensive and time-consuming undertaking, and delivers more contextually relevant outputs. LISA offers RAG through Amazon OpenSearch or PostgreSQL’s PGVector extension on Amazon RDS. -* Non-RAG Model Context: Users can upload documents to their chat sessions to enhance responses or support use cases +* **Non-RAG Model Context:** Users can upload documents to their chat sessions to enhance responses or support use cases like document summarization. -* Model Management: Administrators can add, remove, and update models configured with LISA through the model management +* **Model Management:** Administrators can add, remove, and update models configured with LISA through the model management configuration page or APIs. -* OpenAI API spec: LISA can be configured with compatible tooling. For example, customers can configure LISA as the - model provider for the Continue plugin, an open-source AI code assistance for JetBrains and Visual Studio Code +* **OpenAI API spec:** LISA can be configured with compatible tooling. For example, customers can configure LISA as the + model provider for the [Continue](https://www.continue.dev/) plugin, an open-source AI code assistance for JetBrains and Visual Studio Code integrated development environments (IDEs). This allows users to select from any LISA-configured model to support LLM prompting directly in their IDE. -* Libraries: If your workflow includes libraries such as [LangChain](https://python.langchain.com/) +* **Libraries:** If your workflow includes libraries such as [LangChain](https://python.langchain.com/) or [OpenAI](https://github.com/openai/openai-python), then you can place LISA in your application by changing only the endpoint and headers for the client objects. -* FedRAMP: The AWS services that LISA leverages are FedRAMP High compliant. -* Ongoing Releases: We offer on-going release with new functionality. LISA’s roadmap is customer driven. +* **FedRAMP:** The AWS services that LISA leverages are FedRAMP High compliant. +* **Ongoing Releases:** We offer on-going release with new functionality. LISA’s roadmap is customer driven. ## Deployment Prerequisites @@ -70,7 +72,8 @@ API integrations with each provider. For detailed instructions on setting up, configuring, and deploying LISA, please refer to our separate documentation on installation and usage. -[![LISA Documentation](https://img.shields.io/badge/LISA%20Documentation-blue?style=for-the-badge&logo=Vite&logoColor=white)](https://awslabs.github.io/LISA/) +- [Deployment Guide](lib/docs/admin/getting-started.md) +- [Configuration](lib/docs/config/configuration.md) ## License diff --git a/lib/docs/admin/architecture.md b/lib/docs/admin/architecture.md index 447043d9..92b4326b 100644 --- a/lib/docs/admin/architecture.md +++ b/lib/docs/admin/architecture.md @@ -1,31 +1,36 @@ -# LLM Inference Solution for Amazon Dedicated Cloud (LISA) +# Architecture Overview -## System Overview +LISA’s major components include: LISA Serve, LISA Chat API, LISA Chatbot, LISA RAG, and LISA Model Management. -LISA is designed using a modular, microservices-based architecture, where each service performs a distinct function. It is composed of three core components: LISA Model Management, LISA Serve, and LISA Chat. Each of these components is responsible for specific functionality and interacts via well-defined API endpoints to ensure scalability, security, and fault tolerance across the system. +**Key Solution Features:** -**Key System Functionalities:** +* **Model Hosting**, LISA serve hosts your models in managed and scalable ECS Clusters. +* **Model Management**, LISA has APIs around deploying, updating, and deleting third party and internally hosted models deployed in your account. +* **Inference Requests**, interact with your models via exposed REST APIs or through the LISA Chatbot UI. +* **Chatbot UI** allows users to seamlessly interact with Models, Model Management, RAG, and Configuration APIs. +* **Retrieval-Augmented Generation (RAG) Operations**, leveraging either OpenSearch and/or PGVector for efficient retrieval of relevant external data to enhance model responses. +* **Authentication and Authorization**, LISA supports customers bringing their own OpenID IDP and the use of DynamoDB stored Tokens to interact with the exposed APIs. -* **Authentication and Authorization** via AWS Cognito or OpenID Connect (OIDC) providers, ensuring secure access to both the REST API and Chat UI through token-based authentication and role-based access control. -* **Model Hosting** on AWS ECS with autoscaling and efficient traffic management using Application Load Balancers (ALBs), providing scalable and high-performance model inference. -* **Model Management** using AWS Step Functions to orchestrate complex workflows for creating, updating, and deleting models, automatically managing underlying ECS infrastructure. -* **Inference Requests** served via both the REST API and the Chat UI, dynamically routing user inputs to the appropriate ECS-hosted models for real-time inference. -* **Chat Interface** enabling users to interact with LISA through a user-friendly web interface, offering seamless real-time model interaction and session continuity. -* **Retrieval-Augmented Generation (RAG) Operations**, leveraging either OpenSearch or PGVector for efficient retrieval of relevant external data to enhance model responses. - -### System Architecture +### Solution Architecture ![LISA Architecture](../assets/LisaArchitecture.png) +- **User Interaction with Chat UI or API:** Users can interact with LISA through the Chat UI or REST API. Each interaction is authenticated using AWS Cognito or OIDC, ensuring secure access. +- **Request Routing:** The API Gateway securely routes user requests to the appropriate backend services, whether for fetching the chat UI, performing RAG operations, or managing models. +- **Model Management:** Administrators can deploy, update, or delete models via the Model Management API, which triggers ECS deployment and scaling workflows. +- **Model Inference:** Inference requests are routed to ECS-hosted models or external models via the LiteLLM proxy. Responses are served back to users through the ALB. +- **RAG Integration:** When RAG is enabled, LISA retrieves relevant documents from OpenSearch or PGVector, augmenting the model's response with external knowledge. +- **Session Continuity:** User session data is stored in DynamoDB, ensuring that users can retrieve and continue previous conversations across multiple interactions. +- **Autoscaling:** ECS tasks automatically scale based on system load, with ALBs distributing traffic across available instances to ensure performance. ## LISA Components ### LISA Model Management ![LISA Model Management Architecture](../assets/LisaModelManagement.png) -The Model Management component is responsible for managing the entire lifecycle of models in LISA. This includes creation, updating, deletion, and scaling of models deployed on ECS. The system automates and scales these operations, ensuring that the underlying infrastructure is managed efficiently. +The Model Management component is responsible for managing the entire lifecycle of models in LISA. This includes creation, updating, deletion of models deployed on ECS or third party provided. The service integration automates and scales these operations, ensuring that the underlying infrastructure is managed efficiently. -* **Model Hosting**: Models are containerized and deployed on AWS ECS, with each model hosted in its own isolated ECS task. This design allows models to be independently scaled based on demand. Traffic to the models is balanced using Application Load Balancers (ALBs), ensuring that the autoscaling mechanism reacts to load fluctuations in real time, optimizing both performance and availability. -* **External Model Routing**: LISA utilizes the LiteLLM proxy to route traffic to different model providers, no matter their API and payload format. Users may add models from external providers, such as SageMaker or Bedrock, to their system to allow requests to models hosted in those systems and services. LISA will simply add the configuration to LiteLLM without creating any additional supporting infrastructure. -* **Model Lifecycle Management**: AWS Step Functions are used to orchestrate the lifecycle of models, handling the creation, update, and deletion workflows. Each workflow provisions the required resources using CloudFormation templates, which manage infrastructure components like EC2 instances, security groups, and ECS services. The system ensures that the necessary security, networking, and infrastructure components are automatically deployed and configured. +* **Self-Hosted Models**: Models are containerized and deployed on AWS ECS, with each model hosted in its own isolated ECS task. This design allows models to be independently scaled based on demand. Traffic to the models is balanced using Application Load Balancers (ALBs), ensuring that the autoscaling mechanism reacts to load fluctuations in real time, optimizing both performance and availability. +* **External Model Routing**: LISA utilizes the LiteLLM proxy to route traffic to different model providers, no matter their API and payload format. Users may add models from external providers, such as SageMaker or Bedrock, to LISA. LISA will simply add the configuration to LiteLLM without creating any additional supporting infrastructure. Customers do not have to independently manage the API integration with the use of LiteLLM. +* **Model Lifecycle Management**: AWS Step Functions are used to orchestrate the lifecycle of models, handling the creation, update, and deletion workflows. Each workflow provisions the required resources using CloudFormation templates, which manage infrastructure components like EC2 instances, security groups, and ECS services. LISA ensures that the necessary security, networking, and infrastructure components are automatically deployed and configured. * The CloudFormation stacks define essential resources using the LISA core VPC configuration, ensuring best practices for security and access across all resources in the environment. * DynamoDB stores model metadata, while Amazon S3 securely manages model weights, enabling ECS instances to retrieve the weights dynamically during deployment. @@ -61,14 +66,3 @@ LISA Chat provides a customizable chat interface that enables users to interact * The Chat UI is implemented in the ```lib/user-interface/react/``` folder and is deployed using the scripts in the ```scripts/``` folder. * Session management logic is handled in ```lambda/session/lambda_functions.py```, where session data is stored and retrieved from DynamoDB. * RAG operations are defined in lambda/repository/lambda_functions.py - - -## Interaction Flow - -1. **User Interaction with Chat UI or API:** Users can interact with LISA through the Chat UI or REST API. Each interaction is authenticated using AWS Cognito or OIDC, ensuring secure access. -1. **Request Routing:** The API Gateway securely routes user requests to the appropriate backend services, whether for fetching the chat UI, performing RAG operations, or managing models. -1. **Model Management:** Administrators can deploy, update, or delete models via the Model Management API, which triggers ECS deployment and scaling workflows. -1. **Model Inference:** Inference requests are routed to ECS-hosted models or external models via the LiteLLM proxy. Responses are served back to users through the ALB. -1. **RAG Integration:** When RAG is enabled, LISA retrieves relevant documents from OpenSearch or PGVector, augmenting the model's response with external knowledge. -1. **Session Continuity:** User session data is stored in DynamoDB, ensuring that users can retrieve and continue previous conversations across multiple interactions. -1. **Autoscaling:** ECS tasks automatically scale based on system load, with ALBs distributing traffic across available instances to ensure performance. diff --git a/lib/docs/admin/getting-started.md b/lib/docs/admin/getting-started.md index df2ffe26..8fef51bb 100644 --- a/lib/docs/admin/getting-started.md +++ b/lib/docs/admin/getting-started.md @@ -1,24 +1,27 @@ # Getting Started with LISA -LISA (LLM Inference Solution for Amazon Dedicated Cloud) is an advanced infrastructure solution for deploying and -managing Large Language Models (LLMs) on AWS. This guide will walk you through the setup process, from prerequisites -to deployment. - -## Prerequisites - -Before beginning, ensure you have: - -1. An AWS account with appropriate permissions. - 1. Because of all the resource creation that happens as part of CDK deployments, we expect Administrator or Administrator-like permissions with resource creation and mutation permissions. - Installation will not succeed if this profile does not have permissions to create and edit arbitrary resources for the system. - **Note**: This level of permissions is not required for the runtime of LISA, only its deployment and subsequent updates. -2. AWS CLI installed and configured -3. Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles -4. Python 3.9 or later -5. Node.js 14 or later -6. Docker/Finch installed and running -7. Sufficient disk space for model downloads and conversions +LISA is an infrastructure-as-code solution that leverages AWS services. Customers deploy LISA directly into an AWS account. + +## Deployment Prerequisites + +### Pre-Deployment Steps + +* Set up and have access to an AWS account with appropriate permissions + * All the resource creation that happens as part of CDK deployments expects Administrator or Administrator-like permissions with resource creation and mutation permissions. Installation will not succeed if this profile does not have permissions to create and edit arbitrary resources for the system. Note: This level of permissions is not required for the runtime of LISA. This is only necessary for deployment and subsequent updates. +* Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles +* Optional: If using the chat UI, Have your Identity Provider (IdP) information and access +* Optional: Have your VPC information available, if you are using an existing one for your deployment +* Note: CDK briefly leverages SSM. Confirm it is approved for use by your organization before beginning. + +### Software + +* AWS CLI installed and configured +* Python 3.9 or later +* Node.js 14 or later +* Docker installed and running +* Sufficient disk space for model downloads and conversions + If you're new to CDK, review the [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) and consult with your AWS support team. diff --git a/lib/docs/admin/overview.md b/lib/docs/admin/overview.md index d1857b0d..223205ba 100644 --- a/lib/docs/admin/overview.md +++ b/lib/docs/admin/overview.md @@ -39,25 +39,3 @@ API integrations with each provider. application by changing only the endpoint and headers for the client objects. * FedRAMP: The AWS services that LISA leverages are FedRAMP High compliant. * Ongoing Releases: We offer on-going release with new functionality. LISA’s roadmap is customer driven. - -## Deployment Prerequisites - -### Pre-Deployment Steps - -* Set up and have access to an AWS account with appropriate permissions - * All the resource creation that happens as part of CDK deployments expects Administrator or Administrator-like - permissions with resource creation and mutation permissions. Installation will not succeed if this profile does - not have permissions to create and edit arbitrary resources for the system. Note: This level of permissions is not - required for the runtime of LISA. This is only necessary for deployment and subsequent updates. -* Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles -* Optional: If using the chat UI, Have your Identity Provider (IdP) information and access -* Optional: Have your VPC information available, if you are using an existing one for your deployment -* Note: CDK briefly leverages SSM. Confirm it is approved for use by your organization before beginning. - -### Software - -* AWS CLI installed and configured -* Python 3.9 or later -* Node.js 14 or later -* Docker installed and running -* Sufficient disk space for model downloads and conversions From 6f7f6fdf15be9c75c731edc517f20856c1a95833 Mon Sep 17 00:00:00 2001 From: github_actions_lisa Date: Fri, 15 Nov 2024 18:17:06 +0000 Subject: [PATCH 45/48] Updating version for release v3.2.0 --- VERSION | 2 +- lib/user-interface/react/package.json | 2 +- lisa-sdk/pyproject.toml | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index fd2a0186..944880fa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 +3.2.0 diff --git a/lib/user-interface/react/package.json b/lib/user-interface/react/package.json index 19759667..61069de6 100644 --- a/lib/user-interface/react/package.json +++ b/lib/user-interface/react/package.json @@ -1,7 +1,7 @@ { "name": "lisa-web", "private": true, - "version": "3.1.0", + "version": "3.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/lisa-sdk/pyproject.toml b/lisa-sdk/pyproject.toml index 4bd844ef..20dcfd30 100644 --- a/lisa-sdk/pyproject.toml +++ b/lisa-sdk/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "lisapy" -version = "3.1.0" +version = "3.2.0" description = "A simple SDK to help you interact with LISA. LISA is an LLM hosting solution for AWS dedicated clouds or ADCs." authors = ["Steve Goley "] readme = "README.md" diff --git a/package.json b/package.json index 292cf808..7166d5e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lisa", - "version": "3.1.0", + "version": "3.2.0", "bin": { "lisa": "bin/lisa.js" }, From 27423daf213ce95a9c799cb7b121dcbd4cea01fe Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 15 Nov 2024 12:51:46 -0700 Subject: [PATCH 46/48] v3.2.0 changelog --- CHANGELOG.md | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab04ea73..199dc844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,62 @@ +# v3.2.0 +## Key Features +### Enhanced Deployment Configuration +- LISA v3.2.0 introduces a significant update to the configuration file schema, optimizing the deployment process +- The previous single config.yaml file has been replaced with a more flexible two-file system: config-base.yaml and config-custom.yaml +- config-base.yaml now contains default properties, which can be selectively overridden using config-custom.yaml, allowing for greater customization while maintaining a standardized base configuration +- The number of required properties in the config-custom.yaml file has been reduced to 8 items, simplifying the configuration process +- This update enhances the overall flexibility and maintainability of LISA configurations, providing a more robust foundation for future developments and easier customization for end-users + +#### Important Note +- The previous config.yaml file format is no longer compatible with this update +- To facilitate migration, we have developed a utility. Users can execute `npm run migrate-properties` to automatically convert their existing config.yaml file to the new config-custom.yaml format + +### Admin UI Configuration Page +- Administrative Control of Chat Components: + - Administrators now have granular control over the activation and deactivation of chat components for all users through the Configuration Page + - This feature allows for dynamic management of user interface elements, enhancing system flexibility and user experience customization + - Items that can be configured include: + - The option to delete session history + - Visibility of message metadata + - Configuration of chat Kwargs + - Customization of prompt templates + - Adjust chat history buffer settings + - Modify the number of RAG documents to be included in the retrieval process (TopK) + - Ability to upload RAG documents + - Ability to upload in-context documents +- System Banner Management: + - The Configuration Page now includes functionality for administrators to manage the system banner + - Administrators can activate, deactivate, and update the content of the system banner + +### LISA Documentation Site +- We are pleased to announce the launch of the official [LISA Documentation site](https://awslabs.github.io/LISA/) +- This comprehensive resource provides customers with additional guides and extensive information on LISA +- The documentation is also optionally deployable within your environment during LISA deployment +- The team is continuously working to add and expand content available on this site + +## Enhancements +- Implemented a selection-based interface for instance input, replacing free text entry +- Improved CDK Nag integration across stacks +- Added functionality for administrators to specify block volume size for models, enabling successful deployment of larger models +- Introduced options for administrators to choose between Private or Regional API Gateway endpoints +- Enabled subnet specification within the designated VPC for deployed resources +- Implemented support for headless deployment execution + +## Bug Fixes +- Resolved issues with Create and Update model alerts to ensure proper display in the modal +- Enhanced error handling for model creation/update processes to cover all potential scenarios + +## Coming Soon +- Version 3.3.0 will include a new RAG ingestion pipeline. This will allow users to configure an S3 bucket and an ingestion trigger. When triggered, these documents will be pre-processed and loaded into the selected vector store. + +## Acknowledgements +* @bedanley +* @estohlmann +* @dustins + +**Full Changelog**: https://github.com/awslabs/LISA/compare/v3.1.0...v3.2.0 + + # v3.1.0 ## Enhancements ### Model Management Administration From 1f323504287d530efd17689281739605562f201a Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 15 Nov 2024 14:20:35 -0700 Subject: [PATCH 47/48] ssm language update --- README.md | 2 +- lib/docs/admin/getting-started.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bda31e4b..56f1b5ab 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ API integrations with each provider. * Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles * Optional: If using the chat UI, Have your Identity Provider (IdP) information and access * Optional: Have your VPC information available, if you are using an existing one for your deployment -* Note: CDK briefly leverages SSM. Confirm it is approved for use by your organization before beginning. +* Note: CDK and Model Management both leverage AWS Systems Manager Agent (SSM) parameter store. Confirm that SSM is approved for use by your organization before beginning. ### Software diff --git a/lib/docs/admin/getting-started.md b/lib/docs/admin/getting-started.md index 8fef51bb..1c828188 100644 --- a/lib/docs/admin/getting-started.md +++ b/lib/docs/admin/getting-started.md @@ -12,7 +12,7 @@ LISA is an infrastructure-as-code solution that leverages AWS services. Customer * Familiarity with AWS Cloud Development Kit (CDK) and infrastructure-as-code principles * Optional: If using the chat UI, Have your Identity Provider (IdP) information and access * Optional: Have your VPC information available, if you are using an existing one for your deployment -* Note: CDK briefly leverages SSM. Confirm it is approved for use by your organization before beginning. +* Note: CDK and Model Management both leverage AWS Systems Manager Agent (SSM) parameter store. Confirm that SSM is approved for use by your organization before beginning. ### Software From d6eec7fcc28f2f3de9fbc6a28dae284ecd092883 Mon Sep 17 00:00:00 2001 From: Evan Stohlmann Date: Fri, 15 Nov 2024 14:37:11 -0700 Subject: [PATCH 48/48] LISA language update --- lib/docs/.vitepress/config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts index f08bc2d2..8f69311b 100644 --- a/lib/docs/.vitepress/config.mts +++ b/lib/docs/.vitepress/config.mts @@ -20,7 +20,7 @@ const navLinks = [ { text: 'System Administrator Guide', items: [ - { text: 'What is Lisa?', link: '/admin/overview' }, + { text: 'What is LISA?', link: '/admin/overview' }, { text: 'Architecture Overview', link: '/admin/architecture' }, { text: 'Getting Started', link: '/admin/getting-started' }, { text: 'Deployment', link: '/admin/deploy' },

    wa@?+&Dt{EKv_Q&4i|&_ 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�>%4K1ch>dffmvqh6Ka6%guFg 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 literal 0 HcmV?d00001 diff --git a/lib/docs/assets/LisaServe.png b/lib/docs/assets/LisaServe.png new file mode 100644 index 0000000000000000000000000000000000000000..22365e94f2a2b606bd6eab3de124e458c239b698 GIT binary patch literal 139509 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 Zn^uQ-S1dO0Y0Rp)CWGTf3 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