Merge remote-tracking branch 'origin/zustand/io/migration' into globalVariables

This commit is contained in:
Lucas Oliveira 2024-03-22 16:21:02 +01:00
commit 0f867dc485
46 changed files with 1157 additions and 485 deletions

View file

@ -73,12 +73,12 @@ LANGFLOW_SUPERUSER_PASSWORD=
# STORE_URL # STORE_URL
# Example: LANGFLOW_STORE_URL=https://api.langflow.store # Example: LANGFLOW_STORE_URL=https://api.langflow.store
LANGFLOW_STORE_URL= # LANGFLOW_STORE_URL=
# DOWNLOAD_WEBHOOK_URL # DOWNLOAD_WEBHOOK_URL
# #
LANGFLOW_DOWNLOAD_WEBHOOK_URL= # LANGFLOW_DOWNLOAD_WEBHOOK_URL=
# LIKE_WEBHOOK_URL # LIKE_WEBHOOK_URL
# #
LANGFLOW_LIKE_WEBHOOK_URL= # LANGFLOW_LIKE_WEBHOOK_URL=

View file

@ -15,6 +15,7 @@ COPY ./ ./
# Install dependencies # Install dependencies
RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi 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"] CMD ["sh", "./container-cmd-cdk.sh"]

View file

@ -1,3 +1,5 @@
export LANGFLOW_DATABASE_URL="mysql+pymysql://${username}:${password}@${host}:3306/${dbname}" export LANGFLOW_DATABASE_URL="mysql+pymysql://${username}:${password}@${host}:3306/${dbname}"
# echo $LANGFLOW_DATABASE_URL # echo $LANGFLOW_DATABASE_URL
uvicorn --factory src.backend.langflow.main:create_app --host 0.0.0.0 --port 7860 --reload --log-level debug 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

17
poetry.lock generated
View file

@ -6976,6 +6976,21 @@ files = [
[package.dependencies] [package.dependencies]
six = ">=1.5" 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]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.0.0" version = "1.0.0"
@ -9969,4 +9984,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10,<3.12" python-versions = ">=3.10,<3.12"
content-hash = "13fbf814f41f22cddd661bce5c37abcd36b50fad392d70271597c358694886bc" content-hash = "dedb98bf70db438c28d18a8289a9bb61c79d4ff983b96b7f1d52e11b9c32900b"

View file

@ -108,6 +108,7 @@ unstructured = { extras = ["md"], version = "^0.12.4" }
dspy-ai = "^2.4.0" dspy-ai = "^2.4.0"
crewai = "^0.22.5" crewai = "^0.22.5"
langchain-anthropic = "^0.1.4" langchain-anthropic = "^0.1.4"
python-docx = "^1.1.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest-asyncio = "^0.23.1" pytest-asyncio = "^0.23.1"

View file

@ -8,10 +8,9 @@ Langflow on AWS では、 [AWS Cloud Development Kit](https://aws.amazon.com/cdk
作成するアプリケーションのアーキテクチャです。 作成するアプリケーションのアーキテクチャです。
![langflow-archi](./img/langflow-archi.png) ![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) によって管理されます。 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)を開きます。 1. [AWS CloudShell](https://us-east-1.console.aws.amazon.com/cloudshell/home?region=us-east-1)を開きます。
@ -28,7 +27,7 @@ Fargate のタスクはフロントエンドとバックエンドに分かれて
1. 以下のコマンドを実行します。 1. 以下のコマンドを実行します。
```shell ```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 cd langflow/scripts/aws
cp .env.example .env # 環境設定を変える場合はこのファイル(.env)を編集してください。 cp .env.example .env # 環境設定を変える場合はこのファイル(.env)を編集してください。
npm ci npm ci
@ -38,7 +37,7 @@ Fargate のタスクはフロントエンドとバックエンドに分かれて
1. 表示される URL にアクセスします。 1. 表示される URL にアクセスします。
```shell ```shell
Outputs: Outputs:
LangflowAppStack.NetworkURLXXXXXX = http://alb-XXXXXXXXXXX.elb.amazonaws.com LangflowAppStack.frontendURLXXXXXX = https://XXXXXXXXXXX.cloudfront.net
``` ```
1. サインイン画面でユーザー名とパスワードを入力します。`.env`ファイルでユーザー名とパスワードを設定していない場合、ユーザー名は`admin`、パスワードは`123456`で設定されます。 1. サインイン画面でユーザー名とパスワードを入力します。`.env`ファイルでユーザー名とパスワードを設定していない場合、ユーザー名は`admin`、パスワードは`123456`で設定されます。
![make-cloud9](./img/langflow-signin.png) ![make-cloud9](./img/langflow-signin.png)

View file

@ -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: The architecture of the application to be created:
![langflow-archi](./img/langflow-archi.png) ![langflow-archi](./img/langflow-archi.png)
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.
[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. Fargate uses a Docker image stored in [Amazon Elastic Container Registry](https://aws.amazon.com/ecr/?nc1=h_ls) (ECR).
The aurora's secrets are managed by [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/?nc2=type_a). Aurora's secret is 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.
# How to set up your environment and deploy langflow # 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 ```shell
git clone https://github.com/aws-samples/cloud9-setup-for-prototyping git clone https://github.com/aws-samples/cloud9-setup-for-prototyping
cd cloud9-setup-for-prototyping cd cloud9-setup-for-prototyping
cat params.json | jq '.name |= "c9-for-langflow"'
./bin/bootstrap ./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) ![make-cloud9](./img/langflow-cloud9-en.png)
1. Run the following command in the Cloud9 terminal. 1. Run the following command in the Cloud9 terminal.
```shell ```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 cd langflow/scripts/aws
cp .env.example .env # Edit this file if you need environment settings cp .env.example .env # Edit this file if you need environment settings
npm ci 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. 1. Access the URL displayed.
```shell ```shell
Outputs: 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`. 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) ![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 ```shell
bash delete-resources.sh 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) ![delete-cfn](./img/langflow-cfn.png)
s

View file

@ -4,6 +4,7 @@ import * as cdk from 'aws-cdk-lib';
import { LangflowAppStack } from '../lib/cdk-stack'; import { LangflowAppStack } from '../lib/cdk-stack';
const app = new cdk.App(); const app = new cdk.App();
new LangflowAppStack(app, 'LangflowAppStack', { new LangflowAppStack(app, 'LangflowAppStack', {
/* If you don't specify 'env', this stack will be environment-agnostic. /* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work, * Account/Region-dependent features and context lookups will not work,

View file

@ -17,6 +17,8 @@
] ]
}, },
"context": { "context": {
"ragEnabled": false,
"kendraIndexArn": null,
"@aws-cdk/aws-lambda:recognizeLayerVersion": true, "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true, "@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [ "@aws-cdk/core:target-partitions": [

View file

@ -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-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" # aws ecr describe-repositories --output json | jq -re ".repositories[].repositoryName"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Before After
Before After

View file

@ -2,21 +2,38 @@ import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs'; import { Construct } from 'constructs';
import * as ecs from 'aws-cdk-lib/aws-ecs' 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'; // 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 { export class LangflowAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) { constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props); 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 // Arch
const arch = ecs.CpuArchitecture.X86_64 const arch = ecs.CpuArchitecture.X86_64
// VPC // 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 // ECR
const { ecrFrontEndRepository,ecrBackEndRepository} = new EcrRepository(this, 'Ecr', { const { ecrBackEndRepository } = new EcrRepository(this, 'Ecr', {
cloudmapNamespace:cloudmapNamespace,
arch:arch arch:arch
}) })
@ -25,7 +42,7 @@ export class LangflowAppStack extends cdk.Stack {
const { rdsCluster } = new Rds(this, 'Rds', { vpc, dbSG }) const { rdsCluster } = new Rds(this, 'Rds', { vpc, dbSG })
// IAM // IAM
const { frontendTaskRole, frontendTaskExecutionRole, backendTaskRole, backendTaskExecutionRole } = new EcsIAM(this, 'EcsIAM',{ const { backendTaskRole, backendTaskExecutionRole } = new EcsIAM(this, 'EcsIAM',{
rdsCluster:rdsCluster rdsCluster:rdsCluster
}) })
@ -36,29 +53,18 @@ export class LangflowAppStack extends cdk.Stack {
backendTaskRole:backendTaskRole, backendTaskRole:backendTaskRole,
backendTaskExecutionRole:backendTaskExecutionRole, backendTaskExecutionRole:backendTaskExecutionRole,
backendLogGroup:backendLogGroup, backendLogGroup:backendLogGroup,
cloudmapNamespace:cloudmapNamespace,
rdsCluster:rdsCluster, rdsCluster:rdsCluster,
alb:alb, arch:arch,
arch:arch albTG:albTG
}) })
backendService.node.addDependency(rdsCluster); backendService.node.addDependency(rdsCluster);
const frontendService = new FrontEndCluster(this, 'frontend',{ const frontendService = new Web(this, 'frontend',{
cluster:cluster, cluster:cluster,
ecsFrontSG:ecsFrontSG, alb:alb,
ecrFrontEndRepository:ecrFrontEndRepository, albSG:albSG
targetGroup: targetGroup,
backendServiceName: backendService.backendServiceName,
frontendTaskRole: frontendTaskRole,
frontendTaskExecutionRole: frontendTaskExecutionRole,
frontendLogGroup: frontendLogGroup,
cloudmapNamespace: cloudmapNamespace,
arch:arch
}) })
frontendService.node.addDependency(backendService); frontendService.node.addDependency(backendService);
// S3+CloudFront
// new Web(this,'Cloudfront-S3')
} }
} }

View file

@ -21,18 +21,17 @@ interface BackEndProps {
backendTaskRole: iam.Role; backendTaskRole: iam.Role;
backendTaskExecutionRole: iam.Role; backendTaskExecutionRole: iam.Role;
backendLogGroup: logs.LogGroup; backendLogGroup: logs.LogGroup;
cloudmapNamespace: servicediscovery.PrivateDnsNamespace;
rdsCluster:rds.DatabaseCluster rdsCluster:rds.DatabaseCluster
alb:elb.IApplicationLoadBalancer
arch:ecs.CpuArchitecture arch:ecs.CpuArchitecture
albTG: elb.ApplicationTargetGroup;
} }
export class BackEndCluster extends Construct { export class BackEndCluster extends Construct {
readonly backendServiceName: string
constructor(scope: Construct, id: string, props:BackEndProps) { constructor(scope: Construct, id: string, props:BackEndProps) {
super(scope, id) super(scope, id)
const containerPort = 7860 const backendServiceName = 'backend'
const backendServicePort = 7860
// Secrets ManagerからDB認証情報を取ってくる // Secrets ManagerからDB認証情報を取ってくる
const secretsDB = props.rdsCluster.secret!; const secretsDB = props.rdsCluster.secret!;
@ -59,20 +58,13 @@ export class BackEndCluster extends Construct {
logGroup: props.backendLogGroup, logGroup: props.backendLogGroup,
}), }),
environment:{ 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_AUTO_LOGIN" : process.env.LANGFLOW_AUTO_LOGIN ?? 'false',
"LANGFLOW_SUPERUSER" : process.env.LANGFLOW_SUPERUSER ?? "admin", "LANGFLOW_SUPERUSER" : process.env.LANGFLOW_SUPERUSER ?? "admin",
"LANGFLOW_SUPERUSER_PASSWORD" : process.env.LANGFLOW_SUPERUSER_PASSWORD ?? "123456" "LANGFLOW_SUPERUSER_PASSWORD" : process.env.LANGFLOW_SUPERUSER_PASSWORD ?? "123456"
}, },
portMappings: [ portMappings: [
{ {
containerPort: containerPort, containerPort: backendServicePort,
protocol: ecs.Protocol.TCP, protocol: ecs.Protocol.TCP,
}, },
], ],
@ -84,22 +76,15 @@ export class BackEndCluster extends Construct {
"password": ecs.Secret.fromSecretsManager(secretsDB, 'password'), "password": ecs.Secret.fromSecretsManager(secretsDB, 'password'),
}, },
}); });
this.backendServiceName = 'backend'
const backendService = new ecs.FargateService(this, 'BackEndService', { const backendService = new ecs.FargateService(this, 'BackEndService', {
cluster: props.cluster, cluster: props.cluster,
serviceName: this.backendServiceName, serviceName: backendServiceName,
taskDefinition: backendTaskDefinition, taskDefinition: backendTaskDefinition,
enableExecuteCommand: true, enableExecuteCommand: true,
securityGroups: [props.ecsBackSG], 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 }, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
}); });
props.albTG.addTarget(backendService);
} }
} }

View file

@ -9,12 +9,10 @@ import { Construct } from 'constructs'
interface ECRProps { interface ECRProps {
cloudmapNamespace: servicediscovery.PrivateDnsNamespace;
arch:ecs.CpuArchitecture; arch:ecs.CpuArchitecture;
} }
export class EcrRepository extends Construct { export class EcrRepository extends Construct {
readonly ecrFrontEndRepository: ecr.Repository
readonly ecrBackEndRepository: ecr.Repository readonly ecrBackEndRepository: ecr.Repository
constructor(scope: Construct, id: string, props: ECRProps) { 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 imagePlatform = props.arch == ecs.CpuArchitecture.ARM64 ? Platform.LINUX_ARM64 : Platform.LINUX_AMD64
const backendPath = path.join(__dirname, "../../../../../", "langflow") const backendPath = path.join(__dirname, "../../../../../", "langflow")
const frontendPath = path.join(__dirname, "../../../../src/", "frontend")
const excludeDir = ['node_modules','.git', 'cdk.out'] const excludeDir = ['node_modules','.git', 'cdk.out']
const LifecycleRule = { const LifecycleRule = {
tagStatus: ecr.TagStatus.ANY, tagStatus: ecr.TagStatus.ANY,
@ -30,31 +27,16 @@ export class EcrRepository extends Construct {
maxImageCount: 30, maxImageCount: 30,
} }
// リポジトリ作成 // Backend ECR リポジトリ作成
this.ecrFrontEndRepository = new ecr.Repository(scope, 'LangflowFrontEndRepository', {
repositoryName: 'langflow-frontend-repository',
removalPolicy: RemovalPolicy.RETAIN,
imageScanOnPush: true,
})
this.ecrBackEndRepository = new ecr.Repository(scope, 'LangflowBackEndRepository', { this.ecrBackEndRepository = new ecr.Repository(scope, 'LangflowBackEndRepository', {
repositoryName: 'langflow-backend-repository', repositoryName: 'langflow-backend-repository',
removalPolicy: RemovalPolicy.RETAIN, removalPolicy: RemovalPolicy.RETAIN,
imageScanOnPush: true, imageScanOnPush: true,
}) })
// LifecycleRule作成 // LifecycleRule作成
this.ecrFrontEndRepository.addLifecycleRule(LifecycleRule)
this.ecrBackEndRepository.addLifecycleRule(LifecycleRule) this.ecrBackEndRepository.addLifecycleRule(LifecycleRule)
// Create Docker Image Asset // 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", { const dockerBackEndImageAsset = new DockerImageAsset(this, "DockerBackEndImageAsset", {
directory: backendPath, directory: backendPath,
file:"cdk.Dockerfile", file:"cdk.Dockerfile",
@ -62,12 +44,6 @@ export class EcrRepository extends Construct {
platform: imagePlatform, 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 // Deploy Docker Image to ECR Repository
new ecrdeploy.ECRDeployment(this, "DeployBackEndImage", { new ecrdeploy.ECRDeployment(this, "DeployBackEndImage", {
src: new ecrdeploy.DockerImageName(dockerBackEndImageAsset.imageUri), src: new ecrdeploy.DockerImageName(dockerBackEndImageAsset.imageUri),

View file

@ -1,117 +1,141 @@
import { Duration } from 'aws-cdk-lib' import { Stack, Duration, RemovalPolicy, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs' import { Construct } from 'constructs';
import { import {
aws_ec2 as ec2, aws_ec2 as ec2,
aws_ecs as ecs, aws_ecs as ecs,
aws_ecr as ecr, aws_s3 as s3,
aws_servicediscovery as servicediscovery,
aws_iam as iam, aws_iam as iam,
aws_logs as logs, aws_logs as logs,
aws_elasticloadbalancingv2 as elb, aws_elasticloadbalancingv2 as elb,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
aws_s3_deployment as s3_deployment
} from 'aws-cdk-lib'; } 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 cluster:ecs.Cluster
ecsFrontSG:ec2.SecurityGroup alb:elb.IApplicationLoadBalancer;
ecrFrontEndRepository:ecr.Repository albSG:ec2.SecurityGroup;
targetGroup: elb.ApplicationTargetGroup;
backendServiceName: string;
frontendTaskRole: iam.Role;
frontendTaskExecutionRole: iam.Role;
frontendLogGroup: logs.LogGroup;
cloudmapNamespace: servicediscovery.PrivateDnsNamespace;
arch:ecs.CpuArchitecture;
} }
export class FrontEndCluster extends Construct { export class Web extends Construct {
constructor(scope: Construct, id: string, props:FrontEndProps) { readonly distribution;
constructor(scope: Construct, id: string, props:WebProps) {
super(scope, id) 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 // CDKにて 静的WebサイトをホストするためのAmazon S3バケットを作成
const frontendTaskDefinition = new ecs.FargateTaskDefinition( const websiteBucket = new s3.Bucket(this, 'LangflowWebsiteBucket', commonBucketProps);
this,
'FrontendTaskDef', const originAccessIdentity = new cloudfront.OriginAccessIdentity(
{ this,
memoryLimitMiB: 3072, 'OriginAccessIdentity',
cpu: 1024, {
executionRole: props.frontendTaskExecutionRole, comment: 'langflow-distribution-originAccessIdentity',
runtimePlatform:{ }
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
cpuArchitecture: props.arch,
},
taskRole: props.frontendTaskRole,
}
); );
const frontendServiceName = 'frontend'
frontendTaskDefinition.addContainer('frontendContainer', { const webSiteBucketPolicyStatement = new iam.PolicyStatement({
image: ecs.ContainerImage.fromEcrRepository(props.ecrFrontEndRepository, "latest"), actions: ['s3:GetObject'],
containerName:'langflow-front-container', effect: iam.Effect.ALLOW,
environment: { principals: [
BACKEND_SERVICE_NAME: props.backendServiceName, new iam.CanonicalUserPrincipal(
BACKEND_URL: `http://${props.backendServiceName}.${props.cloudmapNamespace.namespaceName}:7860/`, originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
VITE_PROXY_TARGET: `http://${props.backendServiceName}.${props.cloudmapNamespace.namespaceName}:7860/`, ),
}, ],
logging: ecs.LogDriver.awsLogs({ resources: [`${websiteBucket.bucketArn}/*`],
streamPrefix: 'my-stream',
logGroup: props.frontendLogGroup,
}),
portMappings: [
{
name:frontendServiceName,
containerPort: containerPort,
protocol: ecs.Protocol.TCP,
appProtocol:ecs.AppProtocol.http,
},
],
}); });
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 s3SpaOrigin = new origins.S3Origin(websiteBucket);
// const frontService = new ecs_patterns.ApplicationLoadBalancedFargateService( const ApiSpaOrigin = new origins.LoadBalancerV2Origin(props.alb,{
// this, protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY
// "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 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}`,
});
}
} }

View file

@ -10,8 +10,6 @@ interface IAMProps {
} }
export class EcsIAM extends Construct { export class EcsIAM extends Construct {
readonly frontendTaskRole: iam.Role;
readonly frontendTaskExecutionRole: iam.Role;
readonly backendTaskRole: iam.Role; readonly backendTaskRole: iam.Role;
readonly backendTaskExecutionRole: 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 // BackEnd Task Role
this.backendTaskRole = new iam.Role(this, 'BackendTaskRole', { this.backendTaskRole = new iam.Role(this, 'BackendTaskRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
@ -73,17 +65,6 @@ export class EcsIAM extends Construct {
// KendraとBedrockのアクセス権付与 // KendraとBedrockのアクセス権付与
this.backendTaskRole.attachInlinePolicy(RagAccessPolicy); 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 // BackEnd Task ExecutionRole
this.backendTaskExecutionRole = new iam.Role(this, 'backendTaskExecutionRole', { this.backendTaskExecutionRole = new iam.Role(this, 'backendTaskExecutionRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),

View file

@ -3,4 +3,5 @@ export * from './ecr';
export * from './iam'; export * from './iam';
export * from './frontend'; export * from './frontend';
export * from './backend'; export * from './backend';
export * from './network'; export * from './network';
export * from './kendra';

View file

@ -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'],
})
);
}
}

View file

@ -11,20 +11,16 @@ import {
export class Network extends Construct { export class Network extends Construct {
readonly vpc: ec2.Vpc; readonly vpc: ec2.Vpc;
readonly cluster: ecs.Cluster; 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 ecsBackSG: ec2.SecurityGroup;
readonly dbSG: ec2.SecurityGroup; readonly dbSG: ec2.SecurityGroup;
readonly albSG: ec2.SecurityGroup;
readonly backendLogGroup: logs.LogGroup; 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) { constructor(scope: Construct, id: string) {
super(scope, id) super(scope, id)
const alb_listen_port=80 const alb_listen_port=80
const front_service_port=3000
const back_service_port=7860 const back_service_port=7860
// VPC等リソースの作成 // VPC等リソースの作成
@ -51,22 +47,6 @@ export class Network extends Construct {
], ],
natGateways: 1, 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に設定するセキュリティグループ // ALBに設定するセキュリティグループ
this.albSG = new ec2.SecurityGroup(scope, 'ALBSecurityGroup', { this.albSG = new ec2.SecurityGroup(scope, 'ALBSecurityGroup', {
@ -74,7 +54,6 @@ export class Network extends Construct {
description: 'for alb', description: 'for alb',
vpc: this.vpc, vpc: this.vpc,
}) })
this.albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(alb_listen_port))
this.alb = new elb.ApplicationLoadBalancer(this,'langflow-alb',{ this.alb = new elb.ApplicationLoadBalancer(this,'langflow-alb',{
internetFacing: true, //インターネットからのアクセスを許可するかどうか指定 internetFacing: true, //インターネットからのアクセスを許可するかどうか指定
@ -85,8 +64,8 @@ export class Network extends Construct {
const listener = this.alb.addListener('Listener', { port: alb_listen_port }); const listener = this.alb.addListener('Listener', { port: alb_listen_port });
this.targetGroup = listener.addTargets('targetGroup', { this.albTG = listener.addTargets('targetGroup', {
port: front_service_port, port: back_service_port,
protocol: elb.ApplicationProtocol.HTTP, protocol: elb.ApplicationProtocol.HTTP,
healthCheck: { healthCheck: {
enabled: true, enabled: true,
@ -99,13 +78,12 @@ export class Network extends Construct {
}, },
}); });
// ECS FrontEndに設定するセキュリティグループ // Cluster
this.ecsFrontSG = new ec2.SecurityGroup(scope, 'ECSFrontEndSecurityGroup', { this.cluster = new ecs.Cluster(this, 'EcsCluster', {
securityGroupName: 'langflow-ecs-front-sg', clusterName: 'langflow-cluster',
description: 'for langflow-front-ecs',
vpc: this.vpc, vpc: this.vpc,
}) enableFargateCapacityProviders: true,
this.ecsFrontSG.addIngressRule(this.albSG, ec2.Port.allTcp()) });
// ECS BackEndに設定するセキュリティグループ // ECS BackEndに設定するセキュリティグループ
this.ecsBackSG = new ec2.SecurityGroup(scope, 'ECSBackEndSecurityGroup', { this.ecsBackSG = new ec2.SecurityGroup(scope, 'ECSBackEndSecurityGroup', {
@ -113,7 +91,7 @@ export class Network extends Construct {
description: 'for langflow-back-ecs', description: 'for langflow-back-ecs',
vpc: this.vpc, 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に設定するセキュリティグループ // RDSに設定するセキュリティグループ
this.dbSG = new ec2.SecurityGroup(scope, 'DBSecurityGroup', { this.dbSG = new ec2.SecurityGroup(scope, 'DBSecurityGroup', {
@ -122,7 +100,7 @@ export class Network extends Construct {
description: 'for langflow-db', description: 'for langflow-db',
vpc: this.vpc, 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)) this.dbSG.addIngressRule(this.ecsBackSG, ec2.Port.tcp(3306))
// Create CloudWatch Log Group // Create CloudWatch Log Group
@ -131,13 +109,5 @@ export class Network extends Construct {
removalPolicy: RemovalPolicy.DESTROY, 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}`,
});
} }
} }

View file

@ -8,9 +8,11 @@
"name": "cdk", "name": "cdk",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "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", "cdk-ecr-deployment": "^2.5.30",
"constructs": "^10.0.0", "constructs": "^10.0.0",
"deploy-time-build": "^0.3.12",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
}, },
@ -20,7 +22,7 @@
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.1", "@types/jest": "^29.5.1",
"@types/node": "20.1.7", "@types/node": "20.1.7",
"aws-cdk": "2.86.0", "aws-cdk": "^2.86.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
@ -41,19 +43,324 @@
} }
}, },
"node_modules/@aws-cdk/asset-awscli-v1": { "node_modules/@aws-cdk/asset-awscli-v1": {
"version": "2.2.201", "version": "2.2.202",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.201.tgz", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz",
"integrity": "sha512-INZqcwDinNaIdb5CtW3ez5s943nX5stGBQS6VOP2JDlOFP81hM3fds/9NDknipqfUkZM43dx+HgVvkXYXXARCQ==" "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg=="
}, },
"node_modules/@aws-cdk/asset-kubectl-v20": { "node_modules/@aws-cdk/asset-kubectl-v20": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-kubectl-v20/-/asset-kubectl-v20-2.1.2.tgz",
"integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==" "integrity": "sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg=="
}, },
"node_modules/@aws-cdk/asset-node-proxy-agent-v5": { "node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
"version": "2.0.166", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v5/-/asset-node-proxy-agent-v5-2.0.166.tgz", "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.0.1.tgz",
"integrity": "sha512-j0xnccpUQHXJKPgCwQcGGNu4lRiC1PptYfdxBIH1L4dRK91iBxtSQHESRQX+yB47oGLaF/WfNN/aF3WXwlhikg==" "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": { "node_modules/@babel/code-frame": {
"version": "7.22.13", "version": "7.22.13",
@ -1313,9 +1620,9 @@
} }
}, },
"node_modules/aws-cdk-lib": { "node_modules/aws-cdk-lib": {
"version": "2.86.0", "version": "2.124.0",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.86.0.tgz", "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.124.0.tgz",
"integrity": "sha512-76yZ2MawAGXLD3ox4FjhUIPmAMXteGKkeo3tPMthemusDCCkD2X6DBssXBHjB7r9GnrOMMf8JH5BGq2lOZ539g==", "integrity": "sha512-K/Tey8TMw30GO6UD0qb19CPhBMZhleGshz520ZnbDUJwNfFtejwZOnpmRMOdUP9f4tHc5BrXl1VGsZtXtUaGhg==",
"bundleDependencies": [ "bundleDependencies": [
"@balena/dockerignore", "@balena/dockerignore",
"case", "case",
@ -1329,17 +1636,17 @@
"yaml" "yaml"
], ],
"dependencies": { "dependencies": {
"@aws-cdk/asset-awscli-v1": "^2.2.177", "@aws-cdk/asset-awscli-v1": "^2.2.202",
"@aws-cdk/asset-kubectl-v20": "^2.1.1", "@aws-cdk/asset-kubectl-v20": "^2.1.2",
"@aws-cdk/asset-node-proxy-agent-v5": "^2.0.148", "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1",
"@balena/dockerignore": "^1.0.2", "@balena/dockerignore": "^1.0.2",
"case": "1.6.3", "case": "1.6.3",
"fs-extra": "^11.1.1", "fs-extra": "^11.2.0",
"ignore": "^5.2.4", "ignore": "^5.3.0",
"jsonschema": "^1.4.1", "jsonschema": "^1.4.1",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"punycode": "^2.3.0", "punycode": "^2.3.1",
"semver": "^7.5.1", "semver": "^7.5.4",
"table": "^6.8.1", "table": "^6.8.1",
"yaml": "1.10.2" "yaml": "1.10.2"
}, },
@ -1454,7 +1761,7 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/aws-cdk-lib/node_modules/fs-extra": { "node_modules/aws-cdk-lib/node_modules/fs-extra": {
"version": "11.1.1", "version": "11.2.0",
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1472,7 +1779,7 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/aws-cdk-lib/node_modules/ignore": { "node_modules/aws-cdk-lib/node_modules/ignore": {
"version": "5.2.4", "version": "5.3.0",
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1539,7 +1846,7 @@
} }
}, },
"node_modules/aws-cdk-lib/node_modules/punycode": { "node_modules/aws-cdk-lib/node_modules/punycode": {
"version": "2.3.0", "version": "2.3.1",
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1555,7 +1862,7 @@
} }
}, },
"node_modules/aws-cdk-lib/node_modules/semver": { "node_modules/aws-cdk-lib/node_modules/semver": {
"version": "7.5.2", "version": "7.5.4",
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@ -1624,7 +1931,7 @@
} }
}, },
"node_modules/aws-cdk-lib/node_modules/universalify": { "node_modules/aws-cdk-lib/node_modules/universalify": {
"version": "2.0.0", "version": "2.0.1",
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2400,6 +2707,15 @@
"node": ">=0.10.0" "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": { "node_modules/detect-newline": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",

View file

@ -13,16 +13,18 @@
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.1", "@types/jest": "^29.5.1",
"@types/node": "20.1.7", "@types/node": "20.1.7",
"aws-cdk": "2.86.0", "aws-cdk": "^2.86.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "~5.1.3" "typescript": "~5.1.3"
}, },
"dependencies": { "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", "cdk-ecr-deployment": "^2.5.30",
"constructs": "^10.0.0", "constructs": "^10.0.0",
"deploy-time-build": "^0.3.12",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"source-map-support": "^0.5.21" "source-map-support": "^0.5.21"
} }

View file

@ -10,19 +10,7 @@ from langflow.schema.schema import Record
# Types of files that can be read simply by file.read() # Types of files that can be read simply by file.read()
# and have 100% to be completely readable # and have 100% to be completely readable
TEXT_FILE_TYPES = [ TEXT_FILE_TYPES = ["txt", "md", "mdx", "csv", "json", "yaml", "yml", "xml", "html", "htm", "pdf", "docx"]
"txt",
"md",
"mdx",
"csv",
"json",
"yaml",
"yml",
"xml",
"html",
"htm",
"pdf",
]
def is_hidden(path: Path) -> bool: def is_hidden(path: Path) -> bool:
@ -84,6 +72,13 @@ def read_text_file(file_path: str) -> str:
return f.read() 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: def parse_pdf_to_text(file_path: str) -> str:
from pypdf import PdfReader # type: ignore 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: try:
if file_path.endswith(".pdf"): if file_path.endswith(".pdf"):
text = parse_pdf_to_text(file_path) text = parse_pdf_to_text(file_path)
elif file_path.endswith(".docx"):
text = read_docx_file(file_path)
else: else:
text = read_text_file(file_path) text = read_text_file(file_path)
# if file is json, yaml, or xml, we can parse it # if file is json, yaml, or xml, we can parse it

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional from typing import Any, Dict, List
from langflow import CustomComponent from langflow import CustomComponent
from langflow.base.data.utils import TEXT_FILE_TYPES, parse_text_file_to_record 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): class FileComponent(CustomComponent):
display_name = "File" display_name = "Files"
description = "Load a file." description = "Read Text Files"
def build_config(self) -> Dict[str, Any]: def build_config(self) -> Dict[str, Any]:
return { 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": { "silent_errors": {
"display_name": "Silent Errors", "display_name": "Silent Errors",
"advanced": True, "advanced": True,
@ -19,13 +24,22 @@ class FileComponent(CustomComponent):
}, },
} }
def build( def load_file(self, path: str, silent_errors: bool = False) -> Record:
self,
path: str,
silent_errors: bool = False,
) -> Optional[Record]:
resolved_path = self.resolve_path(path) resolved_path = self.resolve_path(path)
extension = resolved_path.split(".")[-1] 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: if extension not in TEXT_FILE_TYPES:
raise ValueError(f"Unsupported file type: {extension}") 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

View file

@ -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()

View file

@ -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,
)

View file

@ -36,13 +36,12 @@ class MongoDBAtlasSearchComponent(MongoDBAtlasComponent, LCVectorStoreComponent)
mongodb_atlas_cluster_uri: str = "", mongodb_atlas_cluster_uri: str = "",
search_kwargs: Optional[NestedDict] = None, search_kwargs: Optional[NestedDict] = None,
) -> List[Record]: ) -> List[Record]:
search_kwargs = search_kwargs or {}
vector_store = super().build( vector_store = super().build(
connection_string=mongodb_atlas_cluster_uri,
namespace=f"{db_name}.{collection_name}",
embedding=embedding, embedding=embedding,
collection_name=collection_name,
db_name=db_name,
index_name=index_name, index_name=index_name,
mongodb_atlas_cluster_uri=mongodb_atlas_cluster_uri,
search_kwargs=search_kwargs,
) )
if not vector_store: if not vector_store:
raise ValueError("Failed to create MongoDB Atlas Vector Store") raise ValueError("Failed to create MongoDB Atlas Vector Store")

View file

@ -9,13 +9,9 @@ from jose import JWTError, jwt
from sqlmodel import Session from sqlmodel import Session
from starlette.websockets import WebSocket 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.api_key.crud import check_key
from langflow.services.database.models.user.crud import ( from langflow.services.database.models.api_key.model import ApiKey
get_user_by_id, from langflow.services.database.models.user.crud import get_user_by_id, get_user_by_username, update_user_last_login_at
get_user_by_username,
update_user_last_login_at,
)
from langflow.services.database.models.user.model import User from langflow.services.database.models.user.model import User
from langflow.services.deps import get_session, get_settings_service 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): if isinstance(token, Coroutine):
token = await token 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 raise credentials_exception
try: try:
payload = jwt.decode( payload = jwt.decode(
token, token,
settings_service.auth_settings.SECRET_KEY, settings_service.auth_settings.SECRET_KEY.get_secret_value(),
algorithms=[settings_service.auth_settings.ALGORITHM], algorithms=[settings_service.auth_settings.ALGORITHM],
) )
user_id: UUID = payload.get("sub") # type: ignore user_id: UUID = payload.get("sub") # type: ignore
@ -183,7 +179,7 @@ def create_token(data: dict, expires_delta: timedelta):
return jwt.encode( return jwt.encode(
to_encode, to_encode,
settings_service.auth_settings.SECRET_KEY, settings_service.auth_settings.SECRET_KEY.get_secret_value(),
algorithm=settings_service.auth_settings.ALGORITHM, algorithm=settings_service.auth_settings.ALGORITHM,
) )
@ -287,7 +283,7 @@ def create_refresh_token(refresh_token: str, db: Session = Depends(get_session))
try: try:
payload = jwt.decode( payload = jwt.decode(
refresh_token, refresh_token,
settings_service.auth_settings.SECRET_KEY, settings_service.auth_settings.SECRET_KEY.get_secret_value(),
algorithms=[settings_service.auth_settings.ALGORITHM], algorithms=[settings_service.auth_settings.ALGORITHM],
) )
user_id: UUID = payload.get("sub") # type: ignore 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)): 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 # It's important that your secret key is 32 url-safe base64-encoded byte
padded_secret_key = add_padding(SECRET_KEY) padded_secret_key = add_padding(SECRET_KEY)
fernet = Fernet(padded_secret_key) fernet = Fernet(padded_secret_key)

View file

@ -1,23 +1,23 @@
import secrets import secrets
from pathlib import Path 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 ( from langflow.services.settings.constants import (
DEFAULT_SUPERUSER, DEFAULT_SUPERUSER,
DEFAULT_SUPERUSER_PASSWORD, DEFAULT_SUPERUSER_PASSWORD,
) )
from langflow.services.settings.utils import read_secret_from_file, write_secret_to_file 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): class AuthSettings(BaseSettings):
# Login settings # Login settings
CONFIG_DIR: str CONFIG_DIR: str
SECRET_KEY: str = Field( SECRET_KEY: SecretStr = Field(
default="", default=None,
description="Secret key for JWT. If not provided, a random one will be generated.", description="Secret key for JWT. If not provided, a random one will be generated.",
frozen=False, frozen=False,
) )
@ -26,7 +26,6 @@ class AuthSettings(BaseSettings):
REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 12 * 7 REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 12 * 7
# API Key to execute /process endpoint # API Key to execute /process endpoint
API_KEY_SECRET_KEY: Optional[str] = "b82818e0ad4ff76615c5721ee21004b07d84cd9b87ba4d9cb42374da134b841a"
API_KEY_ALGORITHM: str = "HS256" API_KEY_ALGORITHM: str = "HS256"
API_V1_STR: str = "/api/v1" API_V1_STR: str = "/api/v1"

View file

@ -3,6 +3,7 @@ import "reactflow/dist/style.css";
import "./App.css"; import "./App.css";
import { ErrorBoundary } from "react-error-boundary"; import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import ErrorAlert from "./alerts/error"; import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice"; import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success"; import SuccessAlert from "./alerts/success";
@ -47,6 +48,9 @@ export default function App() {
(state) => state.setGlobalVariables (state) => state.setGlobalVariables
); );
const checkHasStore = useStoreStore((state) => state.checkHasStore); const checkHasStore = useStoreStore((state) => state.checkHasStore);
const navigate = useNavigate();
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
useEffect(() => { useEffect(() => {
refreshStars(); refreshStars();
@ -67,11 +71,12 @@ export default function App() {
}, [isAuthenticated]); }, [isAuthenticated]);
useEffect(() => { useEffect(() => {
checkApplicationHealth();
// Timer to call getHealth every 5 seconds // Timer to call getHealth every 5 seconds
const timer = setInterval(() => { const timer = setInterval(() => {
getHealth() getHealth()
.then(() => { .then(() => {
if (fetchError) setFetchError(false); onHealthCheck();
}) })
.catch(() => { .catch(() => {
setFetchError(true); 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 ( return (
//need parent component with width and height //need parent component with width and height
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
@ -93,20 +122,29 @@ export default function App() {
}} }}
FallbackComponent={CrashErrorComponent} FallbackComponent={CrashErrorComponent}
> >
{fetchError ? ( <>
<FetchErrorComponent {
description={FETCH_ERROR_DESCRIPION} <FetchErrorComponent
message={FETCH_ERROR_MESSAGE} description={FETCH_ERROR_DESCRIPION}
></FetchErrorComponent> message={FETCH_ERROR_MESSAGE}
) : isLoading ? ( openModal={fetchError}
<div className="loading-page-panel"> setRetry={() => {
<LoadingComponent remSize={50} /> checkApplicationHealth();
</div> }}
) : ( isLoadingHealth={isLoadingHealth}
<> ></FetchErrorComponent>
<Router /> }
</>
)} {isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
</>
</ErrorBoundary> </ErrorBoundary>
<div></div> <div></div>
<div className="app-div"> <div className="app-div">

View file

@ -275,7 +275,8 @@ export default function GenericNode({
name="Play" name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0" 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) ? (
<Xmark <Xmark
isVisible={true} isVisible={true}
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0" className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
@ -301,7 +302,10 @@ export default function GenericNode({
// INACTIVE should have its own class // INACTIVE should have its own class
return "inactive-status"; return "inactive-status";
} }
if (buildStatus === BuildStatus.BUILT && isInvalid) { if (
(buildStatus === BuildStatus.BUILT && isInvalid) ||
buildStatus === BuildStatus.ERROR
) {
return isDark ? "built-invalid-status-dark" : "built-invalid-status"; return isDark ? "built-invalid-status-dark" : "built-invalid-status";
} else if (buildStatus === BuildStatus.BUILDING) { } else if (buildStatus === BuildStatus.BUILDING) {
return "building-status"; return "building-status";

View file

@ -4,6 +4,7 @@ import Loading from "../../../components/ui/loading";
import { FlowType } from "../../../types/flow"; import { FlowType } from "../../../types/flow";
import { MISSED_ERROR_ALERT } from "../../../constants/alerts_constants"; import { MISSED_ERROR_ALERT } from "../../../constants/alerts_constants";
import { BuildStatus } from "../../../constants/enums";
import useAlertStore from "../../../stores/alertStore"; import useAlertStore from "../../../stores/alertStore";
import useFlowStore from "../../../stores/flowStore"; import useFlowStore from "../../../stores/flowStore";
import { validateNodes } from "../../../utils/reactflowUtils"; import { validateNodes } from "../../../utils/reactflowUtils";
@ -22,6 +23,7 @@ export default function BuildTrigger({
const buildFlow = useFlowStore((state) => state.buildFlow); const buildFlow = useFlowStore((state) => state.buildFlow);
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges); const edges = useFlowStore((state) => state.edges);
const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const eventClick = isBuilding ? "pointer-events-none" : ""; const eventClick = isBuilding ? "pointer-events-none" : "";
@ -32,12 +34,15 @@ export default function BuildTrigger({
if (isBuilding) { if (isBuilding) {
return; return;
} }
const errors = validateNodes(nodes, edges); const errorsObjs = validateNodes(nodes, edges);
const errors = errorsObjs.flatMap((errorObj) => errorObj.errors);
if (errors.length > 0) { if (errors.length > 0) {
setErrorData({ setErrorData({
title: MISSED_ERROR_ALERT, title: MISSED_ERROR_ALERT,
list: errors, list: errors,
}); });
const ids = errorsObjs.map((errorObj) => errorObj.id);
updateBuildStatus(ids, BuildStatus.ERROR);
return; return;
} }
const minimumLoadingTime = 200; // in milliseconds const minimumLoadingTime = 200; // in milliseconds

View file

@ -1,16 +1,51 @@
import BaseModal from "../../modals/baseModal";
import { fetchErrorComponentType } from "../../types/components"; import { fetchErrorComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
export default function FetchErrorComponent({ export default function FetchErrorComponent({
message, message,
description, description,
openModal,
setRetry,
isLoadingHealth,
}: fetchErrorComponentType) { }: fetchErrorComponentType) {
return ( return (
<div role="status" className="m-auto flex flex-col items-center"> <>
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent> <BaseModal size="small-h-full" open={openModal} type="modal">
<br></br> <BaseModal.Content>
<span className="text-lg text-almost-medium-blue">{message}</span> <div role="status" className="m-auto flex flex-col items-center">
<span className="text-lg text-almost-medium-blue">{description}</span> <IconComponent
</div> className={`h-16 w-16`}
name="Unplug"
></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">
{description}
</span>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<div className="m-auto">
<Button
disabled={isLoadingHealth}
onClick={() => {
setRetry();
}}
>
{isLoadingHealth ? (
<div>
<IconComponent name={"Loader2"} className={"animate-spin"} />
</div>
) : (
"Retry"
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
</>
); );
} }

View file

@ -26,6 +26,7 @@ export default function PromptAreaComponent({
<div className={disabled ? "pointer-events-none w-full " : " w-full"}> <div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal <GenericModal
id={id} id={id}
field_name={field_name}
readonly={readonly} readonly={readonly}
type={TypeModal.PROMPT} type={TypeModal.PROMPT}
value={value} value={value}

View file

@ -0,0 +1,119 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "../../utils/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props}>
<div className="nopan nodelete nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};

View file

@ -12,4 +12,5 @@ export enum BuildStatus {
TO_BUILD = "TO_BUILD", TO_BUILD = "TO_BUILD",
BUILT = "BUILT", BUILT = "BUILT",
INACTIVE = "INACTIVE", INACTIVE = "INACTIVE",
ERROR = "ERROR",
} }

View file

@ -9,6 +9,12 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "../../components/ui/dialog"; } from "../../components/ui/dialog";
import {
Dialog as Modal,
DialogContent as ModalContent,
} from "../../components/ui/dialog-with-no-close";
import { modalHeaderType } from "../../types/components"; import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
@ -76,6 +82,7 @@ interface BaseModalProps {
disable?: boolean; disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void; onChangeOpenModal?: (open?: boolean) => void;
type?: "modal" | "dialog";
} }
function BaseModal({ function BaseModal({
open, open,
@ -83,6 +90,7 @@ function BaseModal({
children, children,
size = "large", size = "large",
onChangeOpenModal, onChangeOpenModal,
type = "dialog",
}: BaseModalProps) { }: BaseModalProps) {
const headerChild = React.Children.toArray(children).find( const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header (child) => (child as React.ReactElement).type === Header
@ -156,22 +164,43 @@ function BaseModal({
//UPDATE COLORS AND STYLE CLASSSES //UPDATE COLORS AND STYLE CLASSSES
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <>
{triggerChild} {type === "modal" ? (
<DialogContent className={cn(minWidth, "duration-300")}> <Modal open={open} onOpenChange={setOpen}>
<div className="truncate-doubleline word-break-break-word"> {triggerChild}
{headerChild} <ModalContent className={cn(minWidth, "duration-300")}>
</div> <div className="truncate-doubleline word-break-break-word">
<div {headerChild}
className={`flex flex-col ${height!} w-full transition-all duration-300`} </div>
> <div
{ContentChild} className={`flex flex-col ${height!} w-full transition-all duration-300`}
</div> >
{ContentFooter && ( {ContentChild}
<div className="flex flex-row-reverse">{ContentFooter}</div> </div>
)} {ContentFooter && (
</DialogContent> <div className="flex flex-row-reverse">{ContentFooter}</div>
</Dialog> )}
</ModalContent>
</Modal>
) : (
<Dialog open={open} onOpenChange={setOpen}>
{triggerChild}
<DialogContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</DialogContent>
</Dialog>
)}
</>
); );
} }

View file

@ -31,6 +31,7 @@ import {
CHAT_SECOND_INITIAL_TEXT, CHAT_SECOND_INITIAL_TEXT,
LANGFLOW_CHAT_TITLE, LANGFLOW_CHAT_TITLE,
} from "../../constants/constants"; } from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { getBuildStatus } from "../../controllers/API"; import { getBuildStatus } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore"; import useAlertStore from "../../stores/alertStore";
@ -49,6 +50,7 @@ export default function FormModal({
}): JSX.Element { }): JSX.Element {
const nodes = useFlowStore((state) => state.nodes); const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges); const edges = useFlowStore((state) => state.edges);
const updateBuildStatus = useFlowStore((state) => state.updateBuildStatus);
const flowState = useFlowStore((state) => state.flowState); const flowState = useFlowStore((state) => state.flowState);
const setFlowState = useFlowStore((state) => state.setFlowState); const setFlowState = useFlowStore((state) => state.setFlowState);
const [chatValue, setChatValue] = useState(() => { const [chatValue, setChatValue] = useState(() => {
@ -388,7 +390,8 @@ export default function FormModal({
function sendMessage(): void { function sendMessage(): void {
let nodeValidationErrors = validateNodes(nodes, edges); let nodeValidationErrors = validateNodes(nodes, edges);
if (nodeValidationErrors.length === 0) { const errors = nodeValidationErrors.flatMap((error) => error.errors);
if (errors.length === 0) {
setLockChat(true); setLockChat(true);
let inputs = flowState?.input_keys; let inputs = flowState?.input_keys;
setChatValue(""); setChatValue("");
@ -412,8 +415,10 @@ export default function FormModal({
} else { } else {
setErrorData({ setErrorData({
title: INFO_MISSING_ALERT, title: INFO_MISSING_ALERT,
list: nodeValidationErrors, list: errors,
}); });
const ids = nodeValidationErrors.map((error) => error.id);
updateBuildStatus(ids, BuildStatus.ERROR);
} }
} }
function clearChat(): void { function clearChat(): void {

View file

@ -94,6 +94,49 @@ export default function Page({
const [lastSelection, setLastSelection] = const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null); useState<OnSelectionChangeParams | null>(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); const setNode = useFlowStore((state) => state.setNode);
useEffect(() => { useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
@ -104,49 +147,7 @@ export default function Page({
event.key === "g" event.key === "g"
) { ) {
event.preventDefault(); event.preventDefault();
takeSnapshot(); handleGroupNode();
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),
});
}
} }
if ( if (
(event.ctrlKey || event.metaKey) && (event.ctrlKey || event.metaKey) &&
@ -484,58 +485,7 @@ export default function Page({
isVisible={selectionMenuVisible} isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes} nodes={lastSelection?.nodes}
onClick={() => { onClick={() => {
takeSnapshot(); handleGroupNode();
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),
});
}
}} }}
/> />
</ReactFlow> </ReactFlow>

View file

@ -270,19 +270,10 @@ export default function NodeToolbarComponent({
selected && selected &&
isGroup && isGroup &&
(event.ctrlKey || event.metaKey) && (event.ctrlKey || event.metaKey) &&
event.key === "u" event.key === "g"
) { ) {
event.preventDefault(); event.preventDefault();
takeSnapshot(); handleSelectChange("ungroup");
expandGroupNode(
data.id,
updateFlowPosition(position, data.node?.flow!),
data.node!.template,
nodes,
edges,
setNodes,
setEdges
);
} }
if ( if (
selected && selected &&
@ -571,7 +562,7 @@ export default function NodeToolbarComponent({
Ctrl +{" "} Ctrl +{" "}
</span> </span>
)} )}
<span className="absolute right-2 top-[0.43em]">U</span> <span className="absolute right-2 top-[0.43em]">G</span>
</div> </div>
</SelectItem> </SelectItem>
)} )}

View file

@ -438,16 +438,20 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const setErrorData = useAlertStore.getState().setErrorData; const setErrorData = useAlertStore.getState().setErrorData;
const setNoticeData = useAlertStore.getState().setNoticeData; const setNoticeData = useAlertStore.getState().setNoticeData;
function validateSubgraph(nodes: string[]) { function validateSubgraph(nodes: string[]) {
const errors = validateNodes( const errorsObjs = validateNodes(
get().nodes.filter((node) => nodes.includes(node.id)), get().nodes.filter((node) => nodes.includes(node.id)),
get().edges get().edges
); );
const errors = errorsObjs.map((obj) => obj.errors).flat();
if (errors.length > 0) { if (errors.length > 0) {
setErrorData({ setErrorData({
title: MISSED_ERROR_ALERT, title: MISSED_ERROR_ALERT,
list: errors, list: errors,
}); });
get().setIsBuilding(false); 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"); throw new Error("Invalid nodes");
} }
} }
@ -538,7 +542,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
.filter(Boolean) as string[]; .filter(Boolean) as string[];
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING); useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
}, },
validateNodes: validateSubgraph, onValidateNodes: validateSubgraph,
}); });
get().setIsBuilding(false); get().setIsBuilding(false);
get().revertBuiltStatusFromBuilding(); get().revertBuiltStatusFromBuilding();

View file

@ -88,6 +88,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
} }
}) })
.catch((e) => { .catch((e) => {
set({ isLoading: false });
useAlertStore.getState().setErrorData({ useAlertStore.getState().setErrorData({
title: "Could not load flows from database", title: "Could not load flows from database",
}); });

View file

@ -27,10 +27,6 @@ export const useTypesStore = create<TypesStoreType>((set, get) => ({
resolve(); resolve();
}) })
.catch((error) => { .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.error("An error has occurred while fetching types.");
console.log(error); console.log(error);
reject(); reject();

View file

@ -672,6 +672,9 @@ export type ApiKey = {
export type fetchErrorComponentType = { export type fetchErrorComponentType = {
message: string; message: string;
description: string; description: string;
openModal?: boolean;
setRetry: () => void;
isLoadingHealth: boolean;
}; };
export type dropdownButtonPropsType = { export type dropdownButtonPropsType = {

View file

@ -20,7 +20,7 @@ type BuildVerticesParams = {
onBuildComplete?: (allNodesValid: boolean) => void; onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void; onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
onBuildStart?: (idList: VertexLayerElementType[]) => void; onBuildStart?: (idList: VertexLayerElementType[]) => void;
validateNodes?: (nodes: string[]) => void; onValidateNodes?: (nodes: string[]) => void;
}; };
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI { function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
@ -114,7 +114,7 @@ export async function buildVertices({
onBuildComplete, onBuildComplete,
onBuildError, onBuildError,
onBuildStart, onBuildStart,
validateNodes, onValidateNodes,
}: BuildVerticesParams) { }: BuildVerticesParams) {
let verticesBuild = useFlowStore.getState().verticesBuild; let verticesBuild = useFlowStore.getState().verticesBuild;
// if startNodeId and stopNodeId are provided // if startNodeId and stopNodeId are provided
@ -133,10 +133,10 @@ export async function buildVertices({
if (onGetOrderSuccess) onGetOrderSuccess(); if (onGetOrderSuccess) onGetOrderSuccess();
if (validateNodes) { if (onValidateNodes) {
try { try {
const nodes = useFlowStore.getState().nodes; const nodes = useFlowStore.getState().nodes;
validateNodes(nodes.map((node) => node.id)); onValidateNodes(nodes.map((node) => node.id));
} catch (e) { } catch (e) {
return; return;
} }

View file

@ -313,27 +313,40 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
) { ) {
if (hasDuplicateKeys(template[t].value)) if (hasDuplicateKeys(template[t].value))
errors.push( errors.push(
`${type} (${getFieldTitle( `${displayName || type} (${getFieldTitle(
template, template,
t t
)}) contains duplicate keys with the same values.` )}) contains duplicate keys with the same values.`
); );
if (hasEmptyKey(template[t].value)) if (hasEmptyKey(template[t].value))
errors.push( errors.push(
`${type} (${getFieldTitle(template, t)}) field must not be empty.` `${displayName || type} (${getFieldTitle(
template,
t
)}) field must not be empty.`
); );
} }
return errors; return errors;
}, [] as string[]); }, [] 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<string> }> {
if (nodes.length === 0) { if (nodes.length === 0) {
return [ 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[]) { export function updateEdges(edges: Edge[]) {