diff --git a/.env.example b/.env.example index 31363bd3f..26f6e3a29 100644 --- a/.env.example +++ b/.env.example @@ -73,12 +73,12 @@ LANGFLOW_SUPERUSER_PASSWORD= # STORE_URL # Example: LANGFLOW_STORE_URL=https://api.langflow.store -LANGFLOW_STORE_URL= +# LANGFLOW_STORE_URL= # DOWNLOAD_WEBHOOK_URL # -LANGFLOW_DOWNLOAD_WEBHOOK_URL= +# LANGFLOW_DOWNLOAD_WEBHOOK_URL= # LIKE_WEBHOOK_URL # -LANGFLOW_LIKE_WEBHOOK_URL= \ No newline at end of file +# LANGFLOW_LIKE_WEBHOOK_URL= \ No newline at end of file diff --git a/cdk.Dockerfile b/cdk.Dockerfile index 670ae49bd..5370c9928 100644 --- a/cdk.Dockerfile +++ b/cdk.Dockerfile @@ -15,6 +15,7 @@ COPY ./ ./ # Install dependencies RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi -RUN poetry add pymysql==1.0.2 +RUN poetry add botocore +RUN poetry add pymysql CMD ["sh", "./container-cmd-cdk.sh"] diff --git a/container-cmd-cdk.sh b/container-cmd-cdk.sh index 3ac6400d8..383dffb6b 100644 --- a/container-cmd-cdk.sh +++ b/container-cmd-cdk.sh @@ -1,3 +1,5 @@ export LANGFLOW_DATABASE_URL="mysql+pymysql://${username}:${password}@${host}:3306/${dbname}" # echo $LANGFLOW_DATABASE_URL -uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --log-level debug \ No newline at end of file +uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --log-level debug + +# python -m langflow run --host 0.0.0.0 --port 7860 \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 083cc0799..293cd70ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6976,6 +6976,21 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-docx" +version = "1.1.0" +description = "Create, read, and update Microsoft Word .docx files." +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-docx-1.1.0.tar.gz", hash = "sha256:5829b722141cf1ab79aedf0c34d9fe9924b29764584c0f2164eb2b02dcdf17c9"}, + {file = "python_docx-1.1.0-py3-none-any.whl", hash = "sha256:bac9773278098a1ddc43a52d84e22f5909c4a3080a624530b3ecb3771b07c6cd"}, +] + +[package.dependencies] +lxml = ">=3.1.0" +typing-extensions = "*" + [[package]] name = "python-dotenv" version = "1.0.0" @@ -9969,4 +9984,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "13fbf814f41f22cddd661bce5c37abcd36b50fad392d70271597c358694886bc" +content-hash = "dedb98bf70db438c28d18a8289a9bb61c79d4ff983b96b7f1d52e11b9c32900b" diff --git a/pyproject.toml b/pyproject.toml index 1db0f14dc..bfbda452b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,6 +108,7 @@ unstructured = { extras = ["md"], version = "^0.12.4" } dspy-ai = "^2.4.0" crewai = "^0.22.5" langchain-anthropic = "^0.1.4" +python-docx = "^1.1.0" [tool.poetry.group.dev.dependencies] pytest-asyncio = "^0.23.1" diff --git a/scripts/aws/README.ja.md b/scripts/aws/README.ja.md index aa3b6c710..b7ad49f21 100644 --- a/scripts/aws/README.ja.md +++ b/scripts/aws/README.ja.md @@ -8,10 +8,9 @@ Langflow on AWS では、 [AWS Cloud Development Kit](https://aws.amazon.com/cdk 作成するアプリケーションのアーキテクチャです。 ![langflow-archi](./img/langflow-archi.png) -AWS CDK によって [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/?nc1=h_ls)、[AWS Fargate](https://aws.amazon.com/fargate/?nc2=type_a)、[Amazon Aurora](https://aws.amazon.com/rds/aurora/?nc2=type_a) を作成します。 +AWS CDK によって Langflow のアプリケーションをデプロイします。アプリケーションは [Amazon CloudFront](https://aws.amazon.com/cloudfront/?nc1=h_ls) を介して配信されます。CloudFront は 2 つのオリジンを有しています。1 つ目は静的な Web サイトを配信するための [Amazon Simple Storage Service](https://aws.amazon.com/s3/?nc1=h_ls) (S3)、2 つ目は バックエンドと通信するための [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/?nc1=h_ls) (ALB) です。ALB の背後には FastAPI が動作する [AWS Fargate](https://aws.amazon.com/fargate/?nc2=type_a) 、データベースの [Amazon Aurora](https://aws.amazon.com/rds/aurora/?nc2=type_a) が作成されます。 +Fargate は [Amazon Elastic Container Registry](https://aws.amazon.com/ecr/?nc1=h_ls) (ECR) に保存された Docker イメージを使用します。 Auroraのシークレットは [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/?nc2=type_a) によって管理されます。 -Fargate のタスクはフロントエンドとバックエンドに分かれており、サービス検出によって通信します。 -リソースをデプロイするだけであれば、上記の各サービスについて深い知識は必要ありません。 # 環境構築とデプロイ方法 1. [AWS CloudShell](https://us-east-1.console.aws.amazon.com/cloudshell/home?region=us-east-1)を開きます。 @@ -28,7 +27,7 @@ Fargate のタスクはフロントエンドとバックエンドに分かれて 1. 以下のコマンドを実行します。 ```shell - git clone -b aws-cdk https://github.com/logspace-ai/langflow.git + git clone https://github.com/logspace-ai/langflow.git cd langflow/scripts/aws cp .env.example .env # 環境設定を変える場合はこのファイル(.env)を編集してください。 npm ci @@ -38,7 +37,7 @@ Fargate のタスクはフロントエンドとバックエンドに分かれて 1. 表示される URL にアクセスします。 ```shell Outputs: - LangflowAppStack.NetworkURLXXXXXX = http://alb-XXXXXXXXXXX.elb.amazonaws.com + LangflowAppStack.frontendURLXXXXXX = https://XXXXXXXXXXX.cloudfront.net ``` 1. サインイン画面でユーザー名とパスワードを入力します。`.env`ファイルでユーザー名とパスワードを設定していない場合、ユーザー名は`admin`、パスワードは`123456`で設定されます。 ![make-cloud9](./img/langflow-signin.png) diff --git a/scripts/aws/README.md b/scripts/aws/README.md index b388d46d1..031718f75 100644 --- a/scripts/aws/README.md +++ b/scripts/aws/README.md @@ -9,11 +9,9 @@ This tutorial assumes you have an AWS account and basic knowledge of AWS. The architecture of the application to be created: ![langflow-archi](./img/langflow-archi.png) - -[Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/?nc1=h_ls), [AWS Fargate](https://aws.amazon.com/fargate/?nc2=type_a) and [Amazon Aurora](https://aws.amazon.com/rds/aurora/?nc2=type_a) are created by AWS CDK. -The aurora's secrets are managed by [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/?nc2=type_a). -The Fargate task is divided into a frontend and a backend, which communicate through service discovery. -If you just want to deploy resources, you do not need in-depth knowledge of each of the above services. +Langflow is deployed using AWS CDK. The application is distributed via [Amazon CloudFront](https://aws.amazon.com/cloudfront/?nc1=h_ls), which has two origins: the first is [Amazon Simple Storage Service](https://aws.amazon.com/s3/?nc1=h_ls) (S3) for serving a static website, and the second is an [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/?nc1=h_ls) (ALB) for communicating with the backend. [AWS Fargate](https://aws.amazon.com/fargate/?nc2=type_a), where FastAPI runs and [Amazon Aurora](https://aws.amazon.com/rds/aurora/?nc2=type_a), the database, are created behind the ALB. +Fargate uses a Docker image stored in [Amazon Elastic Container Registry](https://aws.amazon.com/ecr/?nc1=h_ls) (ECR). +Aurora's secret is managed by [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/?nc2=type_a). # How to set up your environment and deploy langflow @@ -22,13 +20,14 @@ If you just want to deploy resources, you do not need in-depth knowledge of each ```shell git clone https://github.com/aws-samples/cloud9-setup-for-prototyping cd cloud9-setup-for-prototyping + cat params.json | jq '.name |= "c9-for-langflow"' ./bin/bootstrap ``` -1. When you see `Done!` in Cloudshell, open `cloud9-for-prototyping` from [AWS Cloud9](https://us-east-1.console.aws.amazon.com/cloud9control/home?region=us-east-1#/). +1. When you see `Done!` in Cloudshell, open `c9-for-langflow` from [AWS Cloud9](https://us-east-1.console.aws.amazon.com/cloud9control/home?region=us-east-1#/). ![make-cloud9](./img/langflow-cloud9-en.png) 1. Run the following command in the Cloud9 terminal. ```shell - git clone -b aws-cdk https://github.com/logspace-ai/langflow.git + git clone https://github.com/logspace-ai/langflow.git cd langflow/scripts/aws cp .env.example .env # Edit this file if you need environment settings npm ci @@ -38,7 +37,7 @@ If you just want to deploy resources, you do not need in-depth knowledge of each 1. Access the URL displayed. ```shell Outputs: - LangflowAppStack.NetworkURLXXXXXX = http://alb-XXXXXXXXXXX.elb.amazonaws.com + LangflowAppStack.frontendURLXXXXXX = https://XXXXXXXXXXX.cloudfront.net ``` 1. Enter your user name and password to sign in. If you have not set a user name and password in your `.env` file, the user name will be set to `admin` and the password to `123456`. ![signin-langflow](./img/langflow-signin.png) @@ -49,5 +48,6 @@ If you just want to deploy resources, you do not need in-depth knowledge of each ```shell bash delete-resources.sh ``` -1. Open [AWS CloudFormation](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/getting-started), select `aws-cloud9-cloud9-for-prototyping-XXXX` and delete it. +1. Open [AWS CloudFormation](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/getting-started), select `aws-cloud9-c9-for-langflow-XXXX` and delete it. ![delete-cfn](./img/langflow-cfn.png) +s \ No newline at end of file diff --git a/scripts/aws/bin/cdk.ts b/scripts/aws/bin/cdk.ts index c680f3e98..82b96f649 100644 --- a/scripts/aws/bin/cdk.ts +++ b/scripts/aws/bin/cdk.ts @@ -4,6 +4,7 @@ import * as cdk from 'aws-cdk-lib'; import { LangflowAppStack } from '../lib/cdk-stack'; const app = new cdk.App(); + new LangflowAppStack(app, 'LangflowAppStack', { /* If you don't specify 'env', this stack will be environment-agnostic. * Account/Region-dependent features and context lookups will not work, diff --git a/scripts/aws/cdk.json b/scripts/aws/cdk.json index dc8ab711a..652770784 100644 --- a/scripts/aws/cdk.json +++ b/scripts/aws/cdk.json @@ -17,6 +17,8 @@ ] }, "context": { + "ragEnabled": false, + "kendraIndexArn": null, "@aws-cdk/aws-lambda:recognizeLayerVersion": true, "@aws-cdk/core:checkSecretUsage": true, "@aws-cdk/core:target-partitions": [ diff --git a/scripts/aws/delete-resources.sh b/scripts/aws/delete-resources.sh index e299165bf..bbe1a62b3 100644 --- a/scripts/aws/delete-resources.sh +++ b/scripts/aws/delete-resources.sh @@ -1,4 +1,4 @@ -aws cloudformation delete-stack --stack-name LangflowAppStack +# aws cloudformation delete-stack --stack-name LangflowAppStack aws ecr delete-repository --repository-name langflow-backend-repository --force -aws ecr delete-repository --repository-name langflow-frontend-repository --force +# aws ecr delete-repository --repository-name langflow-frontend-repository --force # aws ecr describe-repositories --output json | jq -re ".repositories[].repositoryName" \ No newline at end of file diff --git a/scripts/aws/img/langflow-archi.png b/scripts/aws/img/langflow-archi.png index 3064a562e..906783374 100644 Binary files a/scripts/aws/img/langflow-archi.png and b/scripts/aws/img/langflow-archi.png differ diff --git a/scripts/aws/lib/cdk-stack.ts b/scripts/aws/lib/cdk-stack.ts index 8b637cfcf..970623118 100644 --- a/scripts/aws/lib/cdk-stack.ts +++ b/scripts/aws/lib/cdk-stack.ts @@ -2,21 +2,38 @@ import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ecs from 'aws-cdk-lib/aws-ecs' -import { Network, EcrRepository, FrontEndCluster, BackEndCluster, Rds, EcsIAM } from './construct'; +import { Network, EcrRepository, Web, BackEndCluster, Rds, EcsIAM, Rag} from './construct'; // import * as sqs from 'aws-cdk-lib/aws-sqs'; +const errorMessageForBooleanContext = (key: string) => { + return `There was an error setting $ {key}. Possible causes are as follows. + - Trying to set it with the -c option instead of changing cdk.json + - cdk.json is set to a value that is not a boolean (e.g. “true” double quotes are not required) + - no items in cdk.json (unset) `; +}; + + export class LangflowAppStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); + // Kendra Enable + const ragEnabled: boolean = this.node.tryGetContext('ragEnabled')!; + if (typeof ragEnabled !== 'boolean') { + throw new Error(errorMessageForBooleanContext('ragEnabled')); + } + if (ragEnabled) { + new Rag(this, 'Rag', { + }); + } + // Arch const arch = ecs.CpuArchitecture.X86_64 // VPC - const { vpc, cluster, alb, targetGroup, cloudmapNamespace, ecsFrontSG, ecsBackSG, dbSG, albSG, backendLogGroup, frontendLogGroup} = new Network(this, 'Network') + const { vpc, cluster, ecsBackSG, dbSG, backendLogGroup, alb, albTG, albSG} = new Network(this, 'Network') // ECR - const { ecrFrontEndRepository,ecrBackEndRepository} = new EcrRepository(this, 'Ecr', { - cloudmapNamespace:cloudmapNamespace, + const { ecrBackEndRepository } = new EcrRepository(this, 'Ecr', { arch:arch }) @@ -25,7 +42,7 @@ export class LangflowAppStack extends cdk.Stack { const { rdsCluster } = new Rds(this, 'Rds', { vpc, dbSG }) // IAM - const { frontendTaskRole, frontendTaskExecutionRole, backendTaskRole, backendTaskExecutionRole } = new EcsIAM(this, 'EcsIAM',{ + const { backendTaskRole, backendTaskExecutionRole } = new EcsIAM(this, 'EcsIAM',{ rdsCluster:rdsCluster }) @@ -36,29 +53,18 @@ export class LangflowAppStack extends cdk.Stack { backendTaskRole:backendTaskRole, backendTaskExecutionRole:backendTaskExecutionRole, backendLogGroup:backendLogGroup, - cloudmapNamespace:cloudmapNamespace, rdsCluster:rdsCluster, - alb:alb, - arch:arch + arch:arch, + albTG:albTG }) backendService.node.addDependency(rdsCluster); - const frontendService = new FrontEndCluster(this, 'frontend',{ + const frontendService = new Web(this, 'frontend',{ cluster:cluster, - ecsFrontSG:ecsFrontSG, - ecrFrontEndRepository:ecrFrontEndRepository, - targetGroup: targetGroup, - backendServiceName: backendService.backendServiceName, - frontendTaskRole: frontendTaskRole, - frontendTaskExecutionRole: frontendTaskExecutionRole, - frontendLogGroup: frontendLogGroup, - cloudmapNamespace: cloudmapNamespace, - arch:arch + alb:alb, + albSG:albSG }) frontendService.node.addDependency(backendService); - - // S3+CloudFront - // new Web(this,'Cloudfront-S3') } } diff --git a/scripts/aws/lib/construct/backend.ts b/scripts/aws/lib/construct/backend.ts index 5c8540bdb..cba31f988 100644 --- a/scripts/aws/lib/construct/backend.ts +++ b/scripts/aws/lib/construct/backend.ts @@ -21,18 +21,17 @@ interface BackEndProps { backendTaskRole: iam.Role; backendTaskExecutionRole: iam.Role; backendLogGroup: logs.LogGroup; - cloudmapNamespace: servicediscovery.PrivateDnsNamespace; rdsCluster:rds.DatabaseCluster - alb:elb.IApplicationLoadBalancer arch:ecs.CpuArchitecture + albTG: elb.ApplicationTargetGroup; } export class BackEndCluster extends Construct { - readonly backendServiceName: string constructor(scope: Construct, id: string, props:BackEndProps) { super(scope, id) - const containerPort = 7860 + const backendServiceName = 'backend' + const backendServicePort = 7860 // Secrets ManagerからDB認証情報を取ってくる const secretsDB = props.rdsCluster.secret!; @@ -59,20 +58,13 @@ export class BackEndCluster extends Construct { logGroup: props.backendLogGroup, }), environment:{ - // user:pass@endpoint:port/dbname - // "LANGFLOW_DATABASE_URL" : `mysql+pymysql://${username}:${password}@${host}:3306/${dbname}`, - // "LANGFLOW_DATABASE_URL" : "sqlite:///./langflow.db", - // "LANGFLOW_LANGCHAIN_CACHE" : "SQLiteCache", - // "LANGFLOW_AUTO_LOGIN" : "false", - // "LANGFLOW_SUPERUSER" : "admin", - // "LANGFLOW_SUPERUSER_PASSWORD" : "1234567" "LANGFLOW_AUTO_LOGIN" : process.env.LANGFLOW_AUTO_LOGIN ?? 'false', "LANGFLOW_SUPERUSER" : process.env.LANGFLOW_SUPERUSER ?? "admin", "LANGFLOW_SUPERUSER_PASSWORD" : process.env.LANGFLOW_SUPERUSER_PASSWORD ?? "123456" }, portMappings: [ { - containerPort: containerPort, + containerPort: backendServicePort, protocol: ecs.Protocol.TCP, }, ], @@ -84,22 +76,15 @@ export class BackEndCluster extends Construct { "password": ecs.Secret.fromSecretsManager(secretsDB, 'password'), }, }); - this.backendServiceName = 'backend' + const backendService = new ecs.FargateService(this, 'BackEndService', { cluster: props.cluster, - serviceName: this.backendServiceName, + serviceName: backendServiceName, taskDefinition: backendTaskDefinition, enableExecuteCommand: true, securityGroups: [props.ecsBackSG], - cloudMapOptions: { - cloudMapNamespace: props.cloudmapNamespace, - containerPort: containerPort, - dnsRecordType: servicediscovery.DnsRecordType.A, - dnsTtl: Duration.seconds(10), - name: this.backendServiceName - }, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, }); - + props.albTG.addTarget(backendService); } } \ No newline at end of file diff --git a/scripts/aws/lib/construct/ecr.ts b/scripts/aws/lib/construct/ecr.ts index 5162547c3..1fb1c1581 100644 --- a/scripts/aws/lib/construct/ecr.ts +++ b/scripts/aws/lib/construct/ecr.ts @@ -9,12 +9,10 @@ import { Construct } from 'constructs' interface ECRProps { - cloudmapNamespace: servicediscovery.PrivateDnsNamespace; arch:ecs.CpuArchitecture; } export class EcrRepository extends Construct { - readonly ecrFrontEndRepository: ecr.Repository readonly ecrBackEndRepository: ecr.Repository constructor(scope: Construct, id: string, props: ECRProps) { @@ -22,7 +20,6 @@ export class EcrRepository extends Construct { const imagePlatform = props.arch == ecs.CpuArchitecture.ARM64 ? Platform.LINUX_ARM64 : Platform.LINUX_AMD64 const backendPath = path.join(__dirname, "../../../../../", "langflow") - const frontendPath = path.join(__dirname, "../../../../src/", "frontend") const excludeDir = ['node_modules','.git', 'cdk.out'] const LifecycleRule = { tagStatus: ecr.TagStatus.ANY, @@ -30,31 +27,16 @@ export class EcrRepository extends Construct { maxImageCount: 30, } - // リポジトリ作成 - this.ecrFrontEndRepository = new ecr.Repository(scope, 'LangflowFrontEndRepository', { - repositoryName: 'langflow-frontend-repository', - removalPolicy: RemovalPolicy.RETAIN, - imageScanOnPush: true, - }) + // Backend ECR リポジトリ作成 this.ecrBackEndRepository = new ecr.Repository(scope, 'LangflowBackEndRepository', { repositoryName: 'langflow-backend-repository', removalPolicy: RemovalPolicy.RETAIN, imageScanOnPush: true, }) // LifecycleRule作成 - this.ecrFrontEndRepository.addLifecycleRule(LifecycleRule) this.ecrBackEndRepository.addLifecycleRule(LifecycleRule) // Create Docker Image Asset - const dockerFrontEndImageAsset = new DockerImageAsset(this, "DockerFrontEndImageAsset", { - directory: frontendPath, - file:"cdk.Dockerfile", - buildArgs:{ - "BACKEND_URL":`http://backend.${props.cloudmapNamespace.namespaceName}:7860` - }, - exclude: excludeDir, - platform: imagePlatform, - }); const dockerBackEndImageAsset = new DockerImageAsset(this, "DockerBackEndImageAsset", { directory: backendPath, file:"cdk.Dockerfile", @@ -62,12 +44,6 @@ export class EcrRepository extends Construct { platform: imagePlatform, }); - // Deploy Docker Image to ECR Repository - new ecrdeploy.ECRDeployment(this, "DeployFrontEndImage", { - src: new ecrdeploy.DockerImageName(dockerFrontEndImageAsset.imageUri), - dest: new ecrdeploy.DockerImageName(this.ecrFrontEndRepository.repositoryUri) - }); - // Deploy Docker Image to ECR Repository new ecrdeploy.ECRDeployment(this, "DeployBackEndImage", { src: new ecrdeploy.DockerImageName(dockerBackEndImageAsset.imageUri), diff --git a/scripts/aws/lib/construct/frontend.ts b/scripts/aws/lib/construct/frontend.ts index 8238ed135..78d516e14 100644 --- a/scripts/aws/lib/construct/frontend.ts +++ b/scripts/aws/lib/construct/frontend.ts @@ -1,117 +1,141 @@ -import { Duration } from 'aws-cdk-lib' -import { Construct } from 'constructs' +import { Stack, Duration, RemovalPolicy, CfnOutput } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; import { aws_ec2 as ec2, aws_ecs as ecs, - aws_ecr as ecr, - aws_servicediscovery as servicediscovery, + aws_s3 as s3, aws_iam as iam, aws_logs as logs, aws_elasticloadbalancingv2 as elb, + aws_cloudfront as cloudfront, + aws_cloudfront_origins as origins, + aws_s3_deployment as s3_deployment } from 'aws-cdk-lib'; -import { CpuArchitecture } from 'aws-cdk-lib/aws-ecs'; +import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3'; +import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'; +import { NodejsBuild } from 'deploy-time-build'; -interface FrontEndProps { +interface WebProps { cluster:ecs.Cluster - ecsFrontSG:ec2.SecurityGroup - ecrFrontEndRepository:ecr.Repository - targetGroup: elb.ApplicationTargetGroup; - backendServiceName: string; - frontendTaskRole: iam.Role; - frontendTaskExecutionRole: iam.Role; - frontendLogGroup: logs.LogGroup; - cloudmapNamespace: servicediscovery.PrivateDnsNamespace; - arch:ecs.CpuArchitecture; + alb:elb.IApplicationLoadBalancer; + albSG:ec2.SecurityGroup; } -export class FrontEndCluster extends Construct { - constructor(scope: Construct, id: string, props:FrontEndProps) { +export class Web extends Construct { + readonly distribution; + constructor(scope: Construct, id: string, props:WebProps) { super(scope, id) + + const commonBucketProps: s3.BucketProps = { + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + encryption: s3.BucketEncryption.S3_MANAGED, + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, + enforceSSL: true, + }; - const containerPort = 3000 - const frontendTaskDefinition = new ecs.FargateTaskDefinition( - this, - 'FrontendTaskDef', - { - memoryLimitMiB: 3072, - cpu: 1024, - executionRole: props.frontendTaskExecutionRole, - runtimePlatform:{ - operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, - cpuArchitecture: props.arch, - }, - taskRole: props.frontendTaskRole, - } + // CDKにて 静的WebサイトをホストするためのAmazon S3バケットを作成 + const websiteBucket = new s3.Bucket(this, 'LangflowWebsiteBucket', commonBucketProps); + + const originAccessIdentity = new cloudfront.OriginAccessIdentity( + this, + 'OriginAccessIdentity', + { + comment: 'langflow-distribution-originAccessIdentity', + } ); - const frontendServiceName = 'frontend' - frontendTaskDefinition.addContainer('frontendContainer', { - image: ecs.ContainerImage.fromEcrRepository(props.ecrFrontEndRepository, "latest"), - containerName:'langflow-front-container', - environment: { - BACKEND_SERVICE_NAME: props.backendServiceName, - BACKEND_URL: `http://${props.backendServiceName}.${props.cloudmapNamespace.namespaceName}:7860/`, - VITE_PROXY_TARGET: `http://${props.backendServiceName}.${props.cloudmapNamespace.namespaceName}:7860/`, - }, - logging: ecs.LogDriver.awsLogs({ - streamPrefix: 'my-stream', - logGroup: props.frontendLogGroup, - }), - portMappings: [ - { - name:frontendServiceName, - containerPort: containerPort, - protocol: ecs.Protocol.TCP, - appProtocol:ecs.AppProtocol.http, - }, - ], + + const webSiteBucketPolicyStatement = new iam.PolicyStatement({ + actions: ['s3:GetObject'], + effect: iam.Effect.ALLOW, + principals: [ + new iam.CanonicalUserPrincipal( + originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId + ), + ], + resources: [`${websiteBucket.bucketArn}/*`], }); - const frontendService = new ecs.FargateService( - this, - 'FrontendService', - { - serviceName: frontendServiceName, - cluster: props.cluster, - desiredCount: 1, - assignPublicIp: false, - taskDefinition: frontendTaskDefinition, - enableExecuteCommand: true, - securityGroups: [props.ecsFrontSG], - cloudMapOptions: { - cloudMapNamespace: props.cloudmapNamespace, - containerPort: containerPort, - dnsRecordType: servicediscovery.DnsRecordType.A, - dnsTtl: Duration.seconds(10), - name: frontendServiceName - }, - healthCheckGracePeriod: Duration.seconds(1000), - } - ); - props.targetGroup.addTarget(frontendService); + websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement); + websiteBucket.grantRead(originAccessIdentity); - // // Create ALB and ECS Fargate Service - // const frontService = new ecs_patterns.ApplicationLoadBalancedFargateService( - // this, - // "FrontEndService", - // { - // cluster: cluster, - // serviceName: 'langflow-frontend-service', - // cpu: 256, - // memoryLimitMiB: 512, - // listenerPort: 80, - // assignPublicIp: true, // Public facing - ALB - // taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, - // securityGroups:[ecsFrontSG], - // taskImageOptions: { - // family: 'langflow-taskdef', - // containerName: 'langflow-front-container', - // image: ecs.ContainerImage.fromEcrRepository(ecrFrontEndRepository, "latest"), - // containerPort: 3000, // L2なので、TargetGroupのportが3000で設定されるはず - // }, - // loadBalancer:alb, - // openListener:false, - // } - // ); + const s3SpaOrigin = new origins.S3Origin(websiteBucket); + const ApiSpaOrigin = new origins.LoadBalancerV2Origin(props.alb,{ + protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY + }); + const albBehaviorOptions = { + origin: ApiSpaOrigin, + allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, + + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.ALLOW_ALL, + cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, + originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER } + + const cloudFrontWebDistribution = new cloudfront.Distribution(this, 'distribution', { + comment: 'langflow-distribution', + defaultRootObject: 'index.html', + errorResponses: [ + { + httpStatus: 403, + responseHttpStatus: 200, + responsePagePath: '/index.html', + }, + { + httpStatus: 404, + responseHttpStatus: 200, + responsePagePath: '/index.html', + }, + ], + defaultBehavior: { origin: s3SpaOrigin }, + additionalBehaviors: { + '/api/v1/*': albBehaviorOptions, + '/health' : albBehaviorOptions, + }, + enableLogging: true, // ログ出力設定 + logBucket: new s3.Bucket(this, 'LogBucket',commonBucketProps), + logFilePrefix: 'distribution-access-logs/', + logIncludesCookies: true, + }); + this.distribution = cloudFrontWebDistribution; + + + new NodejsBuild(this, 'BuildFrontEnd', { + assets: [ + { + path: '../../src/frontend', + exclude: [ + '.git', + '.github', + '.gitignore', + '.prettierignore', + 'build', + 'node_modules' + ], + }, + ], + nodejsVersion:20, + destinationBucket: websiteBucket, + distribution: cloudFrontWebDistribution, + outputSourceDirectory: 'build', + buildCommands: ['npm install', 'npm run build'], + buildEnvironment: { + // VITE_AXIOS_BASE_URL: `https://${this.distribution.domainName}` + }, + }); + + // distribution から backendへのinbound 許可 + const alb_listen_port=80 + props.albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(alb_listen_port)) + const alb_listen_port_443=443 + props.albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(alb_listen_port_443)) + + + new CfnOutput(this, 'URL', { + value: `https://${this.distribution.domainName}`, + }); +} + } \ No newline at end of file diff --git a/scripts/aws/lib/construct/iam.ts b/scripts/aws/lib/construct/iam.ts index 0a40cf340..13949bda5 100644 --- a/scripts/aws/lib/construct/iam.ts +++ b/scripts/aws/lib/construct/iam.ts @@ -10,8 +10,6 @@ interface IAMProps { } export class EcsIAM extends Construct { - readonly frontendTaskRole: iam.Role; - readonly frontendTaskExecutionRole: iam.Role; readonly backendTaskRole: iam.Role; readonly backendTaskExecutionRole: iam.Role; @@ -58,12 +56,6 @@ export class EcsIAM extends Construct { })], }) - // FrontEnd Task Role - this.frontendTaskRole = new iam.Role(this, 'FrontendTaskRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - }); - this.frontendTaskRole.addToPolicy(ECSExecPolicyStatement); - // BackEnd Task Role this.backendTaskRole = new iam.Role(this, 'BackendTaskRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), @@ -73,17 +65,6 @@ export class EcsIAM extends Construct { // KendraとBedrockのアクセス権付与 this.backendTaskRole.attachInlinePolicy(RagAccessPolicy); - // FrontEnd Task ExecutionRole - this.frontendTaskExecutionRole = new iam.Role(this, 'frontendTaskExecutionRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - managedPolicies: [ - { - managedPolicyArn: - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy', - }, - ], - }); - // BackEnd Task ExecutionRole this.backendTaskExecutionRole = new iam.Role(this, 'backendTaskExecutionRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), diff --git a/scripts/aws/lib/construct/index.ts b/scripts/aws/lib/construct/index.ts index 8c2efcb1b..91e2d2c0a 100644 --- a/scripts/aws/lib/construct/index.ts +++ b/scripts/aws/lib/construct/index.ts @@ -3,4 +3,5 @@ export * from './ecr'; export * from './iam'; export * from './frontend'; export * from './backend'; -export * from './network'; \ No newline at end of file +export * from './network'; +export * from './kendra'; \ No newline at end of file diff --git a/scripts/aws/lib/construct/kendra.ts b/scripts/aws/lib/construct/kendra.ts new file mode 100644 index 000000000..80f60ebad --- /dev/null +++ b/scripts/aws/lib/construct/kendra.ts @@ -0,0 +1,141 @@ +import * as kendra from 'aws-cdk-lib/aws-kendra'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import { Duration, Token, Arn } from 'aws-cdk-lib'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Runtime } from 'aws-cdk-lib/aws-lambda'; + +export interface RagProps { +} + +/** + * RAG を実行するためのリソースを作成する + */ +export class Rag extends Construct { + constructor(scope: Construct, id: string, props: RagProps) { + super(scope, id); + + const kendraIndexArnInCdkContext = + this.node.tryGetContext('kendraIndexArn'); + + let kendraIndexArn: string; + let kendraIndexId: string; + + if (kendraIndexArnInCdkContext) { + // 既存の Kendra Index を利用する場合 + kendraIndexArn = kendraIndexArnInCdkContext!; + kendraIndexId = Arn.extractResourceName( + kendraIndexArnInCdkContext, + 'index' + ); + } else { + // 新規に Kendra Index を作成する場合 + const indexRole = new iam.Role(this, 'KendraIndexRole', { + assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'), + }); + + indexRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: ['*'], + actions: ['s3:GetObject'], + }) + ); + + indexRole.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') + ); + + const index = new kendra.CfnIndex(this, 'KendraIndex', { + name: 'langflow-index', + edition: 'DEVELOPER_EDITION', + roleArn: indexRole.roleArn, + }); + + kendraIndexArn = Token.asString(index.getAtt('Arn')); + kendraIndexId = index.ref; + + // WebCrawler を作成 + const webCrawlerRole = new iam.Role(this, 'KendraWebCrawlerRole', { + assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'), + }); + webCrawlerRole.addToPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [kendraIndexArn], + actions: ['kendra:BatchPutDocument', 'kendra:BatchDeleteDocument'], + }) + ); + + new kendra.CfnDataSource(this, 'WebCrawler', { + indexId: kendraIndexId, + name: 'WebCrawler', + type: 'WEBCRAWLER', + roleArn: webCrawlerRole.roleArn, + languageCode: 'ja', + dataSourceConfiguration: { + webCrawlerConfiguration: { + urls: { + seedUrlConfiguration: { + webCrawlerMode: 'HOST_ONLY', + // デモ用に AWS の GenAI 関連のページを取り込む + seedUrls: [ + 'https://aws.amazon.com/jp/what-is/generative-ai/', + 'https://aws.amazon.com/jp/generative-ai/', + 'https://aws.amazon.com/jp/generative-ai/use-cases/', + 'https://aws.amazon.com/jp/bedrock/', + 'https://aws.amazon.com/jp/bedrock/features/', + 'https://aws.amazon.com/jp/bedrock/testimonials/', + ], + }, + }, + crawlDepth: 1, + urlInclusionPatterns: ['https://aws.amazon.com/jp/.*'], + }, + }, + }); + } + + // RAG 関連の API を追加する + // Lambda + const queryFunction = new NodejsFunction(this, 'Query', { + runtime: Runtime.NODEJS_18_X, + entry: './lambda/queryKendra.ts', + timeout: Duration.minutes(15), + bundling: { + // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする + externalModules: [], + }, + environment: { + INDEX_ID: kendraIndexId, + }, + }); + queryFunction.role?.addToPrincipalPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [kendraIndexArn], + actions: ['kendra:Query'], + }) + ); + + const retrieveFunction = new NodejsFunction(this, 'Retrieve', { + runtime: Runtime.NODEJS_18_X, + entry: './lambda/retrieveKendra.ts', + timeout: Duration.minutes(15), + bundling: { + // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする + externalModules: [], + }, + environment: { + INDEX_ID: kendraIndexId, + }, + }); + retrieveFunction.role?.addToPrincipalPolicy( + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + resources: [kendraIndexArn], + actions: ['kendra:Retrieve'], + }) + ); + } +} \ No newline at end of file diff --git a/scripts/aws/lib/construct/network.ts b/scripts/aws/lib/construct/network.ts index 7dcfabaa0..1abd78ddf 100644 --- a/scripts/aws/lib/construct/network.ts +++ b/scripts/aws/lib/construct/network.ts @@ -11,20 +11,16 @@ import { export class Network extends Construct { readonly vpc: ec2.Vpc; readonly cluster: ecs.Cluster; - readonly alb: elb.IApplicationLoadBalancer; - readonly targetGroup: elb.ApplicationTargetGroup; - readonly cloudmapNamespace: servicediscovery.PrivateDnsNamespace; - readonly ecsFrontSG: ec2.SecurityGroup; readonly ecsBackSG: ec2.SecurityGroup; readonly dbSG: ec2.SecurityGroup; - readonly albSG: ec2.SecurityGroup; readonly backendLogGroup: logs.LogGroup; - readonly frontendLogGroup: logs.LogGroup; + readonly alb: elb.IApplicationLoadBalancer; + readonly albTG: elb.ApplicationTargetGroup; + readonly albSG: ec2.SecurityGroup; constructor(scope: Construct, id: string) { super(scope, id) const alb_listen_port=80 - const front_service_port=3000 const back_service_port=7860 // VPC等リソースの作成 @@ -51,22 +47,6 @@ export class Network extends Construct { ], natGateways: 1, }) - // Cluster - this.cluster = new ecs.Cluster(this, 'EcsCluster', { - clusterName: 'langflow-cluster', - vpc: this.vpc, - enableFargateCapacityProviders: true, - }); - - // Private DNS - this.cloudmapNamespace = new servicediscovery.PrivateDnsNamespace( - this, - 'Namespace', - { - name: 'ecs-deploy.com', - vpc: this.vpc, - } - ); // ALBに設定するセキュリティグループ this.albSG = new ec2.SecurityGroup(scope, 'ALBSecurityGroup', { @@ -74,7 +54,6 @@ export class Network extends Construct { description: 'for alb', vpc: this.vpc, }) - this.albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(alb_listen_port)) this.alb = new elb.ApplicationLoadBalancer(this,'langflow-alb',{ internetFacing: true, //インターネットからのアクセスを許可するかどうか指定 @@ -85,8 +64,8 @@ export class Network extends Construct { const listener = this.alb.addListener('Listener', { port: alb_listen_port }); - this.targetGroup = listener.addTargets('targetGroup', { - port: front_service_port, + this.albTG = listener.addTargets('targetGroup', { + port: back_service_port, protocol: elb.ApplicationProtocol.HTTP, healthCheck: { enabled: true, @@ -99,13 +78,12 @@ export class Network extends Construct { }, }); - // ECS FrontEndに設定するセキュリティグループ - this.ecsFrontSG = new ec2.SecurityGroup(scope, 'ECSFrontEndSecurityGroup', { - securityGroupName: 'langflow-ecs-front-sg', - description: 'for langflow-front-ecs', + // Cluster + this.cluster = new ecs.Cluster(this, 'EcsCluster', { + clusterName: 'langflow-cluster', vpc: this.vpc, - }) - this.ecsFrontSG.addIngressRule(this.albSG, ec2.Port.allTcp()) + enableFargateCapacityProviders: true, + }); // ECS BackEndに設定するセキュリティグループ this.ecsBackSG = new ec2.SecurityGroup(scope, 'ECSBackEndSecurityGroup', { @@ -113,7 +91,7 @@ export class Network extends Construct { description: 'for langflow-back-ecs', vpc: this.vpc, }) - this.ecsBackSG.addIngressRule(this.ecsFrontSG, ec2.Port.tcp(back_service_port)) + this.ecsBackSG.addIngressRule(this.albSG,ec2.Port.tcp(back_service_port)) // RDSに設定するセキュリティグループ this.dbSG = new ec2.SecurityGroup(scope, 'DBSecurityGroup', { @@ -122,7 +100,7 @@ export class Network extends Construct { description: 'for langflow-db', vpc: this.vpc, }) - // AppRunnerSecurityGroupからのポート3306:mysql(5432:postgres)のインバウンドを許可 + // langflow-ecs-back-sg からのポート3306:mysql(5432:postgres)のインバウンドを許可 this.dbSG.addIngressRule(this.ecsBackSG, ec2.Port.tcp(3306)) // Create CloudWatch Log Group @@ -131,13 +109,5 @@ export class Network extends Construct { removalPolicy: RemovalPolicy.DESTROY, }); - this.frontendLogGroup = new logs.LogGroup(this, 'frontendLogGroup', { - logGroupName: 'langflow-frontend-logs', - removalPolicy: RemovalPolicy.DESTROY, - }); - - new CfnOutput(this, 'URL', { - value: `http://${this.alb.loadBalancerDnsName}`, - }); } } \ No newline at end of file diff --git a/scripts/aws/package-lock.json b/scripts/aws/package-lock.json index 68344e614..4deefa752 100644 --- a/scripts/aws/package-lock.json +++ b/scripts/aws/package-lock.json @@ -8,9 +8,11 @@ "name": "cdk", "version": "0.1.0", "dependencies": { - "aws-cdk-lib": "^2.86.0", + "@aws-solutions-constructs/aws-cloudfront-s3": "^2.49.0", + "aws-cdk-lib": "^2.124.0", "cdk-ecr-deployment": "^2.5.30", "constructs": "^10.0.0", + "deploy-time-build": "^0.3.12", "dotenv": "^16.3.1", "source-map-support": "^0.5.21" }, @@ -20,7 +22,7 @@ "devDependencies": { "@types/jest": "^29.5.1", "@types/node": "20.1.7", - "aws-cdk": "2.86.0", + "aws-cdk": "^2.86.0", "jest": "^29.5.0", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", @@ -41,19 +43,324 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.201", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.201.tgz", - "integrity": "sha512-INZqcwDinNaIdb5CtW3ez5s943nX5stGBQS6VOP2JDlOFP81hM3fds/9NDknipqfUkZM43dx+HgVvkXYXXARCQ==" + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==" }, "node_modules/@aws-cdk/asset-kubectl-v20": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v5": { - "version": "2.0.166", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.166.tgz", - "integrity": "sha512-j0xnccpUQHXJKPgCwQcGGNu4lRiC1PptYfdxBIH1L4dRK91iBxtSQHESRQX+yB47oGLaF/WfNN/aF3WXwlhikg==" + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz", + "integrity": "sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==" + }, + "node_modules/@aws-solutions-constructs/aws-cloudfront-s3": { + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-s3/-/aws-cloudfront-s3-2.49.0.tgz", + "integrity": "sha512-0VMtl+Ma+aLZo3Nuvq9bTl/Oz9vn5OaPEOELr9pe8kYuoHwGQUGuxXP/xJlQUWbXwR3JCQVcN5k4kdnD5eLXFw==", + "dependencies": { + "@aws-solutions-constructs/core": "2.49.0", + "@aws-solutions-constructs/resources": "2.49.0" + }, + "peerDependencies": { + "@aws-solutions-constructs/core": "2.49.0", + "@aws-solutions-constructs/resources": "2.49.0", + "aws-cdk-lib": "^2.118.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-solutions-constructs/core": { + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/core/-/core-2.49.0.tgz", + "integrity": "sha512-SZ3CBYF6jVQj0DSNwhksqwz5ohW4w+DlgKiZhrO9gLiW4YcRrxFzwQ7I9FHFUDPZOxsN4NmsuQNj/3s02gycmQ==", + "bundleDependencies": [ + "deepmerge", + "npmlog", + "deep-diff" + ], + "dependencies": { + "deep-diff": "^1.0.2", + "deepmerge": "^4.0.0", + "npmlog": "^4.1.2" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.118.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/ansi-regex": { + "version": "2.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/aproba": { + "version": "1.2.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/are-we-there-yet": { + "version": "1.1.7", + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/code-point-at": { + "version": "1.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/console-control-strings": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/core-util-is": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/deep-diff": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/deepmerge": { + "version": "4.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/delegates": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/gauge": { + "version": "2.7.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/has-unicode": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/inherits": { + "version": "2.0.4", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/isarray": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/npmlog": { + "version": "4.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/number-is-nan": { + "version": "1.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/object-assign": { + "version": "4.1.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/process-nextick-args": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/readable-stream": { + "version": "2.3.8", + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/safe-buffer": { + "version": "5.1.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/set-blocking": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/signal-exit": { + "version": "3.0.7", + "inBundle": true, + "license": "ISC" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/string_decoder": { + "version": "1.1.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/string-width": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/strip-ansi": { + "version": "3.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/@aws-solutions-constructs/core/node_modules/wide-align": { + "version": "1.1.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-solutions-constructs/core/node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@aws-solutions-constructs/resources": { + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/resources/-/resources-2.49.0.tgz", + "integrity": "sha512-HHx9tTA36plfVxzzQ+qjHCJOo6ikHR+xEqtb1dDBQvgtWo3SnEbWAKBDqNPA6DGn7PoFhxE0zA0ntj7RfFjeLQ==", + "bundleDependencies": [ + "@aws-sdk/client-kms", + "@aws-sdk/client-s3", + "aws-sdk-client-mock" + ], + "dependencies": { + "@aws-sdk/client-kms": "^3.478.0", + "@aws-sdk/client-s3": "^3.478.0", + "@aws-solutions-constructs/core": "2.49.0", + "aws-sdk-client-mock": "^3.0.0" + }, + "peerDependencies": { + "@aws-solutions-constructs/core": "2.49.0", + "aws-cdk-lib": "^2.118.0", + "constructs": "^10.0.0" + } }, "node_modules/@babel/code-frame": { "version": "7.22.13", @@ -1313,9 +1620,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.86.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.86.0.tgz", - "integrity": "sha512-76yZ2MawAGXLD3ox4FjhUIPmAMXteGKkeo3tPMthemusDCCkD2X6DBssXBHjB7r9GnrOMMf8JH5BGq2lOZ539g==", + "version": "2.124.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.124.0.tgz", + "integrity": "sha512-K/Tey8TMw30GO6UD0qb19CPhBMZhleGshz520ZnbDUJwNfFtejwZOnpmRMOdUP9f4tHc5BrXl1VGsZtXtUaGhg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -1329,17 +1636,17 @@ "yaml" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.177", - "@aws-cdk/asset-kubectl-v20": "^2.1.1", - "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", + "@aws-cdk/asset-awscli-v1": "^2.2.202", + "@aws-cdk/asset-kubectl-v20": "^2.1.2", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.1.1", - "ignore": "^5.2.4", + "fs-extra": "^11.2.0", + "ignore": "^5.3.0", "jsonschema": "^1.4.1", "minimatch": "^3.1.2", - "punycode": "^2.3.0", - "semver": "^7.5.1", + "punycode": "^2.3.1", + "semver": "^7.5.4", "table": "^6.8.1", "yaml": "1.10.2" }, @@ -1454,7 +1761,7 @@ "license": "MIT" }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.1.1", + "version": "11.2.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -1472,7 +1779,7 @@ "license": "ISC" }, "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.2.4", + "version": "5.3.0", "inBundle": true, "license": "MIT", "engines": { @@ -1539,7 +1846,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.0", + "version": "2.3.1", "inBundle": true, "license": "MIT", "engines": { @@ -1555,7 +1862,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.5.2", + "version": "7.5.4", "inBundle": true, "license": "ISC", "dependencies": { @@ -1624,7 +1931,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", "inBundle": true, "license": "MIT", "engines": { @@ -2400,6 +2707,15 @@ "node": ">=0.10.0" } }, + "node_modules/deploy-time-build": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/deploy-time-build/-/deploy-time-build-0.3.12.tgz", + "integrity": "sha512-fVyuwB0Nh7mOLYswhHIHv3NOCeTWyNAjzM8cqSBnuhfvRKSdLZUFPnfwlPk1VgvYp9lfrxBZ+eicjWPBrAr53g==", + "peerDependencies": { + "aws-cdk-lib": "^2.38.0", + "constructs": "^10.0.5" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", diff --git a/scripts/aws/package.json b/scripts/aws/package.json index fb7f23e76..c74884aea 100644 --- a/scripts/aws/package.json +++ b/scripts/aws/package.json @@ -13,16 +13,18 @@ "devDependencies": { "@types/jest": "^29.5.1", "@types/node": "20.1.7", - "aws-cdk": "2.86.0", + "aws-cdk": "^2.86.0", "jest": "^29.5.0", "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "typescript": "~5.1.3" }, "dependencies": { - "aws-cdk-lib": "^2.86.0", + "@aws-solutions-constructs/aws-cloudfront-s3": "^2.49.0", + "aws-cdk-lib": "^2.124.0", "cdk-ecr-deployment": "^2.5.30", "constructs": "^10.0.0", + "deploy-time-build": "^0.3.12", "dotenv": "^16.3.1", "source-map-support": "^0.5.21" } diff --git a/src/backend/langflow/base/data/utils.py b/src/backend/langflow/base/data/utils.py index 450f6e04b..c3fda5e34 100644 --- a/src/backend/langflow/base/data/utils.py +++ b/src/backend/langflow/base/data/utils.py @@ -10,19 +10,7 @@ from langflow.schema.schema import Record # Types of files that can be read simply by file.read() # and have 100% to be completely readable -TEXT_FILE_TYPES = [ - "txt", - "md", - "mdx", - "csv", - "json", - "yaml", - "yml", - "xml", - "html", - "htm", - "pdf", -] +TEXT_FILE_TYPES = ["txt", "md", "mdx", "csv", "json", "yaml", "yml", "xml", "html", "htm", "pdf", "docx"] def is_hidden(path: Path) -> bool: @@ -84,6 +72,13 @@ def read_text_file(file_path: str) -> str: return f.read() +def read_docx_file(file_path: str) -> str: + from docx import Document # type: ignore + + doc = Document(file_path) + return "\n\n".join([p.text for p in doc.paragraphs]) + + def parse_pdf_to_text(file_path: str) -> str: from pypdf import PdfReader # type: ignore @@ -96,6 +91,8 @@ def parse_text_file_to_record(file_path: str, silent_errors: bool) -> Optional[R try: if file_path.endswith(".pdf"): text = parse_pdf_to_text(file_path) + elif file_path.endswith(".docx"): + text = read_docx_file(file_path) else: text = read_text_file(file_path) # if file is json, yaml, or xml, we can parse it diff --git a/src/backend/langflow/components/data/File.py b/src/backend/langflow/components/data/File.py index 4c4e42de4..5d0b2ee56 100644 --- a/src/backend/langflow/components/data/File.py +++ b/src/backend/langflow/components/data/File.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, List from langflow import CustomComponent from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record @@ -6,12 +6,17 @@ from langflow.schema import Record class FileComponent(CustomComponent): - display_name = "File" - description = "Load a file." + display_name = "Files" + description = "Read Text Files" def build_config(self) -> Dict[str, Any]: return { - "path": {"display_name": "Path"}, + "paths": { + "display_name": "Paths", + "field_type": "file", + "file_types": TEXT_FILE_TYPES, + "info": f"Supported file types: {', '.join(TEXT_FILE_TYPES)}", + }, "silent_errors": { "display_name": "Silent Errors", "advanced": True, @@ -19,13 +24,22 @@ class FileComponent(CustomComponent): }, } - def build( - self, - path: str, - silent_errors: bool = False, - ) -> Optional[Record]: + def load_file(self, path: str, silent_errors: bool = False) -> Record: resolved_path = self.resolve_path(path) extension = resolved_path.split(".")[-1] + if extension == "doc": + raise ValueError("doc files are not supported. Please save as .docx") if extension not in TEXT_FILE_TYPES: raise ValueError(f"Unsupported file type: {extension}") - return parse_text_file_to_record(resolved_path, silent_errors) + record = parse_text_file_to_record(resolved_path, silent_errors) + self.status = record if record else "No data" + return record or Record() + + def build( + self, + paths: List[str], + silent_errors: bool = False, + ) -> List[Record]: + records = [self.load_file(path, silent_errors) for path in paths] + self.status = records + return records diff --git a/src/backend/langflow/components/retrievers/VectorStoreRetriever.py b/src/backend/langflow/components/retrievers/VectorStoreRetriever.py new file mode 100644 index 000000000..576b02e69 --- /dev/null +++ b/src/backend/langflow/components/retrievers/VectorStoreRetriever.py @@ -0,0 +1,17 @@ +from langchain_core.vectorstores import VectorStoreRetriever + +from langflow import CustomComponent +from langflow.field_typing import VectorStore + + +class VectoStoreRetrieverComponent(CustomComponent): + display_name = "VectorStore Retriever" + description = "A vector store retriever" + + def build_config(self): + return { + "vectorstore": {"display_name": "Vector Store", "type": VectorStore}, + } + + def build(self, vectorstore: VectorStore) -> VectorStoreRetriever: + return vectorstore.as_retriever() diff --git a/src/backend/langflow/components/tools/RetrieverTool.py b/src/backend/langflow/components/tools/RetrieverTool.py new file mode 100644 index 000000000..5a598dbe2 --- /dev/null +++ b/src/backend/langflow/components/tools/RetrieverTool.py @@ -0,0 +1,32 @@ +from langchain.tools.retriever import create_retriever_tool + +from langflow import CustomComponent +from langflow.field_typing import BaseRetriever, Tool + + +class RetrieverToolComponent(CustomComponent): + display_name = "RetrieverTool" + description = "Tool for interacting with retriever" + + def build_config(self): + return { + "retriever": { + "display_name": "Retriever", + "info": "Retriever to interact with", + "type": BaseRetriever, + }, + "name": {"display_name": "Name", "info": "Name of the tool"}, + "description": {"display_name": "Description", "info": "Description of the tool"}, + } + + def build( + self, + retriever: BaseRetriever, + name: str, + description: str, + ) -> Tool: + return create_retriever_tool( + retriever=retriever, + name=name, + description=description, + ) diff --git a/src/backend/langflow/components/tools/__init__.py b/src/backend/langflow/components/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/langflow/components/vectorstores/MongoDBAtlasVectorSearch.py b/src/backend/langflow/components/vectorstores/MongoDBAtlasVectorSearch.py index 2fa588d89..10b69ce15 100644 --- a/src/backend/langflow/components/vectorstores/MongoDBAtlasVectorSearch.py +++ b/src/backend/langflow/components/vectorstores/MongoDBAtlasVectorSearch.py @@ -36,13 +36,12 @@ class MongoDBAtlasSearchComponent(MongoDBAtlasComponent, LCVectorStoreComponent) mongodb_atlas_cluster_uri: str = "", search_kwargs: Optional[NestedDict] = None, ) -> List[Record]: + search_kwargs = search_kwargs or {} vector_store = super().build( + connection_string=mongodb_atlas_cluster_uri, + namespace=f"{db_name}.{collection_name}", embedding=embedding, - collection_name=collection_name, - db_name=db_name, index_name=index_name, - mongodb_atlas_cluster_uri=mongodb_atlas_cluster_uri, - search_kwargs=search_kwargs, ) if not vector_store: raise ValueError("Failed to create MongoDB Atlas Vector Store") diff --git a/src/backend/langflow/services/auth/utils.py b/src/backend/langflow/services/auth/utils.py index 23ce1e702..6babee5bf 100644 --- a/src/backend/langflow/services/auth/utils.py +++ b/src/backend/langflow/services/auth/utils.py @@ -9,13 +9,9 @@ from jose import JWTError, jwt from sqlmodel import Session from starlette.websockets import WebSocket -from langflow.services.database.models.api_key.model import ApiKey from langflow.services.database.models.api_key.crud import check_key -from langflow.services.database.models.user.crud import ( - get_user_by_id, - get_user_by_username, - update_user_last_login_at, -) +from langflow.services.database.models.api_key.model import ApiKey +from langflow.services.database.models.user.crud import get_user_by_id, get_user_by_username, update_user_last_login_at from langflow.services.database.models.user.model import User from langflow.services.deps import get_session, get_settings_service @@ -107,13 +103,13 @@ async def get_current_user_by_jwt( if isinstance(token, Coroutine): token = await token - if settings_service.auth_settings.SECRET_KEY is None: + if settings_service.auth_settings.SECRET_KEY.get_secret_value() is None: raise credentials_exception try: payload = jwt.decode( token, - settings_service.auth_settings.SECRET_KEY, + settings_service.auth_settings.SECRET_KEY.get_secret_value(), algorithms=[settings_service.auth_settings.ALGORITHM], ) user_id: UUID = payload.get("sub") # type: ignore @@ -183,7 +179,7 @@ def create_token(data: dict, expires_delta: timedelta): return jwt.encode( to_encode, - settings_service.auth_settings.SECRET_KEY, + settings_service.auth_settings.SECRET_KEY.get_secret_value(), algorithm=settings_service.auth_settings.ALGORITHM, ) @@ -287,7 +283,7 @@ def create_refresh_token(refresh_token: str, db: Session = Depends(get_session)) try: payload = jwt.decode( refresh_token, - settings_service.auth_settings.SECRET_KEY, + settings_service.auth_settings.SECRET_KEY.get_secret_value(), algorithms=[settings_service.auth_settings.ALGORITHM], ) user_id: UUID = payload.get("sub") # type: ignore @@ -326,7 +322,7 @@ def add_padding(s): def get_fernet(settings_service=Depends(get_settings_service)): - SECRET_KEY = settings_service.auth_settings.SECRET_KEY + SECRET_KEY = settings_service.auth_settings.SECRET_KEY.get_secret_value() # It's important that your secret key is 32 url-safe base64-encoded byte padded_secret_key = add_padding(SECRET_KEY) fernet = Fernet(padded_secret_key) diff --git a/src/backend/langflow/services/settings/auth.py b/src/backend/langflow/services/settings/auth.py index 8463d0781..11e08b1c4 100644 --- a/src/backend/langflow/services/settings/auth.py +++ b/src/backend/langflow/services/settings/auth.py @@ -1,23 +1,23 @@ import secrets from pathlib import Path -from typing import Optional + +from loguru import logger +from passlib.context import CryptContext +from pydantic import Field, SecretStr, validator +from pydantic_settings import BaseSettings from langflow.services.settings.constants import ( DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD, ) from langflow.services.settings.utils import read_secret_from_file, write_secret_to_file -from loguru import logger -from passlib.context import CryptContext -from pydantic import Field, validator -from pydantic_settings import BaseSettings class AuthSettings(BaseSettings): # Login settings CONFIG_DIR: str - SECRET_KEY: str = Field( - default="", + SECRET_KEY: SecretStr = Field( + default=None, description="Secret key for JWT. If not provided, a random one will be generated.", frozen=False, ) @@ -26,7 +26,6 @@ class AuthSettings(BaseSettings): REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 12 * 7 # API Key to execute /process endpoint - API_KEY_SECRET_KEY: Optional[str] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a" API_KEY_ALGORITHM: str = "HS256" API_V1_STR: str = "/api/v1" diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 46d57008c..da3c50325 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -3,6 +3,7 @@ import "reactflow/dist/style.css"; import "./App.css"; import { ErrorBoundary } from "react-error-boundary"; +import { useNavigate } from "react-router-dom"; import ErrorAlert from "./alerts/error"; import NoticeAlert from "./alerts/notice"; import SuccessAlert from "./alerts/success"; @@ -47,6 +48,9 @@ export default function App() { (state) => state.setGlobalVariables ); const checkHasStore = useStoreStore((state) => state.checkHasStore); + const navigate = useNavigate(); + + const [isLoadingHealth, setIsLoadingHealth] = useState(false); useEffect(() => { refreshStars(); @@ -67,11 +71,12 @@ export default function App() { }, [isAuthenticated]); useEffect(() => { + checkApplicationHealth(); // Timer to call getHealth every 5 seconds const timer = setInterval(() => { getHealth() .then(() => { - if (fetchError) setFetchError(false); + onHealthCheck(); }) .catch(() => { setFetchError(true); @@ -84,6 +89,30 @@ export default function App() { }; }, []); + const checkApplicationHealth = () => { + setIsLoadingHealth(true); + getHealth() + .then(() => { + onHealthCheck(); + }) + .catch(() => { + setFetchError(true); + }); + + setTimeout(() => { + setIsLoadingHealth(false); + }, 2000); + }; + + const onHealthCheck = () => { + setFetchError(false); + //This condition is necessary to avoid infinite loop on starter page when the application is not healthy + if (isLoading === true && window.location.pathname === "/") { + navigate("/flows"); + window.location.reload(); + } + }; + return ( //need parent component with width and height
@@ -93,20 +122,29 @@ export default function App() { }} FallbackComponent={CrashErrorComponent} > - {fetchError ? ( - - ) : isLoading ? ( -
- -
- ) : ( - <> - - - )} + <> + { + { + checkApplicationHealth(); + }} + isLoadingHealth={isLoadingHealth} + > + } + + {isLoading ? ( +
+ +
+ ) : ( + <> + + + )} +
diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index ac12eee3f..904a94b79 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -275,7 +275,8 @@ export default function GenericNode({ name="Play" className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0" /> - ) : validationStatus && !validationStatus.valid ? ( + ) : buildStatus === BuildStatus.ERROR || + (validationStatus && !validationStatus.valid) ? ( state.buildFlow); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); + const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus); const setErrorData = useAlertStore((state) => state.setErrorData); const eventClick = isBuilding ? "pointer-events-none" : ""; @@ -32,12 +34,15 @@ export default function BuildTrigger({ if (isBuilding) { return; } - const errors = validateNodes(nodes, edges); + const errorsObjs = validateNodes(nodes, edges); + const errors = errorsObjs.flatMap((errorObj) => errorObj.errors); if (errors.length > 0) { setErrorData({ title: MISSED_ERROR_ALERT, list: errors, }); + const ids = errorsObjs.map((errorObj) => errorObj.id); + updateBuildStatus(ids, BuildStatus.ERROR); return; } const minimumLoadingTime = 200; // in milliseconds diff --git a/src/frontend/src/components/fetchErrorComponent/index.tsx b/src/frontend/src/components/fetchErrorComponent/index.tsx index 6004d9dfc..956de6270 100644 --- a/src/frontend/src/components/fetchErrorComponent/index.tsx +++ b/src/frontend/src/components/fetchErrorComponent/index.tsx @@ -1,16 +1,51 @@ +import BaseModal from "../../modals/baseModal"; import { fetchErrorComponentType } from "../../types/components"; import IconComponent from "../genericIconComponent"; +import { Button } from "../ui/button"; export default function FetchErrorComponent({ message, description, + openModal, + setRetry, + isLoadingHealth, }: fetchErrorComponentType) { return ( -
- -

- {message} - {description} -
+ <> + + +
+ +

+ {message} + + {description} + +
+
+ + +
+ +
+
+
+ ); } diff --git a/src/frontend/src/components/promptComponent/index.tsx b/src/frontend/src/components/promptComponent/index.tsx index 784e84fdb..40b79a4c4 100644 --- a/src/frontend/src/components/promptComponent/index.tsx +++ b/src/frontend/src/components/promptComponent/index.tsx @@ -26,6 +26,7 @@ export default function PromptAreaComponent({
( + +
+ {children} +
+
+); +DialogPortal.displayName = DialogPrimitive.Portal.displayName; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +}; diff --git a/src/frontend/src/constants/enums.ts b/src/frontend/src/constants/enums.ts index 764115ed6..b8fa125ac 100644 --- a/src/frontend/src/constants/enums.ts +++ b/src/frontend/src/constants/enums.ts @@ -12,4 +12,5 @@ export enum BuildStatus { TO_BUILD = "TO_BUILD", BUILT = "BUILT", INACTIVE = "INACTIVE", + ERROR = "ERROR", } diff --git a/src/frontend/src/modals/baseModal/index.tsx b/src/frontend/src/modals/baseModal/index.tsx index a8a59dad3..08f00ae8d 100644 --- a/src/frontend/src/modals/baseModal/index.tsx +++ b/src/frontend/src/modals/baseModal/index.tsx @@ -9,6 +9,12 @@ import { DialogTitle, DialogTrigger, } from "../../components/ui/dialog"; + +import { + Dialog as Modal, + DialogContent as ModalContent, +} from "../../components/ui/dialog-with-no-close"; + import { modalHeaderType } from "../../types/components"; import { cn } from "../../utils/utils"; @@ -76,6 +82,7 @@ interface BaseModalProps { disable?: boolean; onChangeOpenModal?: (open?: boolean) => void; + type?: "modal" | "dialog"; } function BaseModal({ open, @@ -83,6 +90,7 @@ function BaseModal({ children, size = "large", onChangeOpenModal, + type = "dialog", }: BaseModalProps) { const headerChild = React.Children.toArray(children).find( (child) => (child as React.ReactElement).type === Header @@ -156,22 +164,43 @@ function BaseModal({ //UPDATE COLORS AND STYLE CLASSSES return ( - - {triggerChild} - -
- {headerChild} -
-
- {ContentChild} -
- {ContentFooter && ( -
{ContentFooter}
- )} -
-
+ <> + {type === "modal" ? ( + + {triggerChild} + +
+ {headerChild} +
+
+ {ContentChild} +
+ {ContentFooter && ( +
{ContentFooter}
+ )} +
+
+ ) : ( + + {triggerChild} + +
+ {headerChild} +
+
+ {ContentChild} +
+ {ContentFooter && ( +
{ContentFooter}
+ )} +
+
+ )} + ); } diff --git a/src/frontend/src/modals/formModal/index.tsx b/src/frontend/src/modals/formModal/index.tsx index 9d9b343c1..0348280fe 100644 --- a/src/frontend/src/modals/formModal/index.tsx +++ b/src/frontend/src/modals/formModal/index.tsx @@ -31,6 +31,7 @@ import { CHAT_SECOND_INITIAL_TEXT, LANGFLOW_CHAT_TITLE, } from "../../constants/constants"; +import { BuildStatus } from "../../constants/enums"; import { AuthContext } from "../../contexts/authContext"; import { getBuildStatus } from "../../controllers/API"; import useAlertStore from "../../stores/alertStore"; @@ -49,6 +50,7 @@ export default function FormModal({ }): JSX.Element { const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); + const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus); const flowState = useFlowStore((state) => state.flowState); const setFlowState = useFlowStore((state) => state.setFlowState); const [chatValue, setChatValue] = useState(() => { @@ -388,7 +390,8 @@ export default function FormModal({ function sendMessage(): void { let nodeValidationErrors = validateNodes(nodes, edges); - if (nodeValidationErrors.length === 0) { + const errors = nodeValidationErrors.flatMap((error) => error.errors); + if (errors.length === 0) { setLockChat(true); let inputs = flowState?.input_keys; setChatValue(""); @@ -412,8 +415,10 @@ export default function FormModal({ } else { setErrorData({ title: INFO_MISSING_ALERT, - list: nodeValidationErrors, + list: errors, }); + const ids = nodeValidationErrors.map((error) => error.id); + updateBuildStatus(ids, BuildStatus.ERROR); } } function clearChat(): void { diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index bf3629db8..d4927f2d9 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -94,6 +94,49 @@ export default function Page({ const [lastSelection, setLastSelection] = useState(null); + function handleGroupNode() { + takeSnapshot(); + if (validateSelection(lastSelection!, edges).length === 0) { + const clonedNodes = cloneDeep(nodes); + const clonedEdges = cloneDeep(edges); + const clonedSelection = cloneDeep(lastSelection); + updateIds({ nodes: clonedNodes, edges: clonedEdges }, clonedSelection!); + const { newFlow, removedEdges } = generateFlow( + clonedSelection!, + clonedNodes, + clonedEdges, + getRandomName() + ); + const newGroupNode = generateNodeFromFlow(newFlow, getNodeId); + const newEdges = reconnectEdges(newGroupNode, removedEdges); + setNodes([ + ...clonedNodes.filter( + (oldNodes) => + !clonedSelection?.nodes.some( + (selectionNode) => selectionNode.id === oldNodes.id + ) + ), + newGroupNode, + ]); + setEdges([ + ...clonedEdges.filter( + (oldEdge) => + !clonedSelection!.nodes.some( + (selectionNode) => + selectionNode.id === oldEdge.target || + selectionNode.id === oldEdge.source + ) + ), + ...newEdges, + ]); + } else { + setErrorData({ + title: INVALID_SELECTION_ERROR_ALERT, + list: validateSelection(lastSelection!, edges), + }); + } + } + const setNode = useFlowStore((state) => state.setNode); useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { @@ -104,49 +147,7 @@ export default function Page({ event.key === "g" ) { event.preventDefault(); - takeSnapshot(); - if (validateSelection(lastSelection!, edges).length === 0) { - const clonedNodes = cloneDeep(nodes); - const clonedEdges = cloneDeep(edges); - const clonedSelection = cloneDeep(lastSelection); - updateIds( - { nodes: clonedNodes, edges: clonedEdges }, - clonedSelection! - ); - const { newFlow, removedEdges } = generateFlow( - clonedSelection!, - clonedNodes, - clonedEdges, - getRandomName() - ); - const newGroupNode = generateNodeFromFlow(newFlow, getNodeId); - const newEdges = reconnectEdges(newGroupNode, removedEdges); - setNodes([ - ...clonedNodes.filter( - (oldNodes) => - !clonedSelection?.nodes.some( - (selectionNode) => selectionNode.id === oldNodes.id - ) - ), - newGroupNode, - ]); - setEdges([ - ...clonedEdges.filter( - (oldEdge) => - !clonedSelection!.nodes.some( - (selectionNode) => - selectionNode.id === oldEdge.target || - selectionNode.id === oldEdge.source - ) - ), - ...newEdges, - ]); - } else { - setErrorData({ - title: INVALID_SELECTION_ERROR_ALERT, - list: validateSelection(lastSelection!, edges), - }); - } + handleGroupNode(); } if ( (event.ctrlKey || event.metaKey) && @@ -484,58 +485,7 @@ export default function Page({ isVisible={selectionMenuVisible} nodes={lastSelection?.nodes} onClick={() => { - takeSnapshot(); - if ( - validateSelection(lastSelection!, edges).length === 0 - ) { - const clonedNodes = cloneDeep(nodes); - const clonedEdges = cloneDeep(edges); - const clonedSelection = cloneDeep(lastSelection); - updateIds( - { nodes: clonedNodes, edges: clonedEdges }, - clonedSelection! - ); - const { newFlow, removedEdges } = generateFlow( - clonedSelection!, - clonedNodes, - clonedEdges, - getRandomName() - ); - const newGroupNode = generateNodeFromFlow( - newFlow, - getNodeId - ); - const newEdges = reconnectEdges( - newGroupNode, - removedEdges - ); - setNodes([ - ...clonedNodes.filter( - (oldNodes) => - !clonedSelection?.nodes.some( - (selectionNode) => - selectionNode.id === oldNodes.id - ) - ), - newGroupNode, - ]); - setEdges([ - ...clonedEdges.filter( - (oldEdge) => - !clonedSelection!.nodes.some( - (selectionNode) => - selectionNode.id === oldEdge.target || - selectionNode.id === oldEdge.source - ) - ), - ...newEdges, - ]); - } else { - setErrorData({ - title: INVALID_SELECTION_ERROR_ALERT, - list: validateSelection(lastSelection!, edges), - }); - } + handleGroupNode(); }} /> diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 2c989cdeb..92a3a7395 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -270,19 +270,10 @@ export default function NodeToolbarComponent({ selected && isGroup && (event.ctrlKey || event.metaKey) && - event.key === "u" + event.key === "g" ) { event.preventDefault(); - takeSnapshot(); - expandGroupNode( - data.id, - updateFlowPosition(position, data.node?.flow!), - data.node!.template, - nodes, - edges, - setNodes, - setEdges - ); + handleSelectChange("ungroup"); } if ( selected && @@ -571,7 +562,7 @@ export default function NodeToolbarComponent({ Ctrl +{" "} )} - U + G
)} diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 4b991e829..a7cec5573 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -438,16 +438,20 @@ const useFlowStore = create((set, get) => ({ const setErrorData = useAlertStore.getState().setErrorData; const setNoticeData = useAlertStore.getState().setNoticeData; function validateSubgraph(nodes: string[]) { - const errors = validateNodes( + const errorsObjs = validateNodes( get().nodes.filter((node) => nodes.includes(node.id)), get().edges ); + const errors = errorsObjs.map((obj) => obj.errors).flat(); if (errors.length > 0) { setErrorData({ title: MISSED_ERROR_ALERT, list: errors, }); get().setIsBuilding(false); + const ids = errorsObjs.map((obj) => obj.id).flat(); + console.log("ids", ids); + get().updateBuildStatus(ids, BuildStatus.ERROR); throw new Error("Invalid nodes"); } } @@ -538,7 +542,7 @@ const useFlowStore = create((set, get) => ({ .filter(Boolean) as string[]; useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); }, - validateNodes: validateSubgraph, + onValidateNodes: validateSubgraph, }); get().setIsBuilding(false); get().revertBuiltStatusFromBuilding(); diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 5d6a3c77f..2ca51a4c4 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -88,6 +88,7 @@ const useFlowsManagerStore = create((set, get) => ({ } }) .catch((e) => { + set({ isLoading: false }); useAlertStore.getState().setErrorData({ title: "Could not load flows from database", }); diff --git a/src/frontend/src/stores/typesStore.ts b/src/frontend/src/stores/typesStore.ts index 557a3a30d..808a5d646 100644 --- a/src/frontend/src/stores/typesStore.ts +++ b/src/frontend/src/stores/typesStore.ts @@ -27,10 +27,6 @@ export const useTypesStore = create((set, get) => ({ resolve(); }) .catch((error) => { - useAlertStore.getState().setErrorData({ - title: "An error has occurred while fetching types.", - list: ["Please refresh the page."], - }); console.error("An error has occurred while fetching types."); console.log(error); reject(); diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 3cb1b8780..4395ab24c 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -672,6 +672,9 @@ export type ApiKey = { export type fetchErrorComponentType = { message: string; description: string; + openModal?: boolean; + setRetry: () => void; + isLoadingHealth: boolean; }; export type dropdownButtonPropsType = { diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index e702ec64b..1121fca04 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -20,7 +20,7 @@ type BuildVerticesParams = { onBuildComplete?: (allNodesValid: boolean) => void; onBuildError?: (title, list, idList: VertexLayerElementType[]) => void; onBuildStart?: (idList: VertexLayerElementType[]) => void; - validateNodes?: (nodes: string[]) => void; + onValidateNodes?: (nodes: string[]) => void; }; function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI { @@ -114,7 +114,7 @@ export async function buildVertices({ onBuildComplete, onBuildError, onBuildStart, - validateNodes, + onValidateNodes, }: BuildVerticesParams) { let verticesBuild = useFlowStore.getState().verticesBuild; // if startNodeId and stopNodeId are provided @@ -133,10 +133,10 @@ export async function buildVertices({ if (onGetOrderSuccess) onGetOrderSuccess(); - if (validateNodes) { + if (onValidateNodes) { try { const nodes = useFlowStore.getState().nodes; - validateNodes(nodes.map((node) => node.id)); + onValidateNodes(nodes.map((node) => node.id)); } catch (e) { return; } diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index edd48354e..0c74979a5 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -313,27 +313,40 @@ export function validateNode(node: NodeType, edges: Edge[]): Array { ) { if (hasDuplicateKeys(template[t].value)) errors.push( - `${type} (${getFieldTitle( + `${displayName || type} (${getFieldTitle( template, t )}) contains duplicate keys with the same values.` ); if (hasEmptyKey(template[t].value)) errors.push( - `${type} (${getFieldTitle(template, t)}) field must not be empty.` + `${displayName || type} (${getFieldTitle( + template, + t + )}) field must not be empty.` ); } return errors; }, [] as string[]); } -export function validateNodes(nodes: Node[], edges: Edge[]) { +export function validateNodes( + nodes: Node[], + edges: Edge[] +): // this returns an array of tuples with the node id and the errors +Array<{ id: string; errors: Array }> { if (nodes.length === 0) { return [ - "No nodes found in the flow. Please add at least one node to the flow.", + { + id: "", + errors: [ + "No nodes found in the flow. Please add at least one node to the flow.", + ], + }, ]; } - return nodes.flatMap((n: NodeType) => validateNode(n, edges)); + // validateNode(n, edges) returns an array of errors for the node + return nodes.map((n) => ({ id: n.id, errors: validateNode(n, edges) })); } export function updateEdges(edges: Edge[]) {