0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GenU のバックエンド (CDK) 詳細解説 ⑫GenerativeAiUseCasesStack > RagKnowledgeBase, UseCaseBuilder, Transcribe スタックの確認

Last updated at Posted at 2025-03-27

はじめに

皆さん、こんにちは。

私は業務でデータ利活用基盤を取り扱っていること、2024 AWS Japan Top Engineer に選出されたということから、AWS GenU およびそれに必要なデータ基盤の探求 (Snowflake, dbt, Iceberg, etc) に取り組む必要があると考えています。

本投稿では、GenU のバックエンドである CDK コードを詳細に解説します。
自身そして閲覧して頂いた皆様の GenU への理解が少しでも深まり、生成 AI の民主化につながっていければと考えています。

前回までのおさらい

前回までで、以下が完了しました。

GenU の CDK は最大で以下の 6 つの子スタックを作成します。

  • CloudFrontWafStack
  • RagKnowledgeBaseStack
  • AgentStack
  • GuardrailStack
  • GenerativeAiUseCasesStack ※メインスタック
  • DashboardStack

第 9 回から GenU 内の本丸である GenerativeAiUseCasesStack スタックを解説しています。

GenerativeAiUseCasesStack スタック

GenerativeAiUseCasesStack スタックでは、以下のリソースを作成しています。

image.png

  • Auth
  • Database
  • Api
  • CommonWebAcl
  • Web
  • Rag
  • RagKnowledgeBase
  • UseCaseBuilder
  • Transcribe

今回は RagKnowledgeBase, UseCaseBuilder, Transcribe リソースを解説していきます。

GenerativeAiUseCasesStack > RagKnowledgeBase リソース

RagKnowledgeBase リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

image.png

RagKnowledgeBase では ナレッジベースから情報を取得する API を定義しています。
以下のソースコードが RagKnowledgeBase の定義です。

packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
    // RAG Knowledge Base
    if (params.ragKnowledgeBaseEnabled) {
      const knowledgeBaseId =
        params.ragKnowledgeBaseId || props.knowledgeBaseId;
      if (knowledgeBaseId) {
        new RagKnowledgeBase(this, 'RagKnowledgeBase', {
          modelRegion: params.modelRegion,
          knowledgeBaseId: knowledgeBaseId,
          userPool: auth.userPool,
          api: api.api,
        });
        // File API から data source の Bucket のファイルをダウンロードできるようにする
        if (props.knowledgeBaseDataSourceBucketName) {
          api.allowDownloadFile(props.knowledgeBaseDataSourceBucketName);
        }
      }
    }

RagKnowledgeBase リソースの実体は packages/cdk/lib/construct/rag-knowledge-base-stack.ts にあります。
スタック作成時のパラメータ ragKnowledgeBaseEnabled (デフォルト値は false) が true の場合、以下のコードを実行し RagKnowledgeBase を作成します。

packages/cdk/lib/construct/rag-knowledge-base-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import {
  AuthorizationType,
  CognitoUserPoolsAuthorizer,
  LambdaIntegration,
  RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';

export interface RagKnowledgeBaseProps {
  // Context Params
  modelRegion: string;

  // Resource
  knowledgeBaseId: string;
  userPool: UserPool;
  api: RestApi;
}

export class RagKnowledgeBase extends Construct {
  constructor(scope: Construct, id: string, props: RagKnowledgeBaseProps) {
    super(scope, id);

    const { modelRegion } = props;

    const retrieveFunction = new NodejsFunction(this, 'Retrieve', {
      runtime: Runtime.NODEJS_LATEST,
      entry: './lambda/retrieveKnowledgeBase.ts',
      timeout: cdk.Duration.minutes(15),
      environment: {
        KNOWLEDGE_BASE_ID: props.knowledgeBaseId,
        MODEL_REGION: modelRegion,
      },
    });

    retrieveFunction.role?.addToPrincipalPolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        resources: [
          `arn:aws:bedrock:${modelRegion}:${cdk.Stack.of(this).account}:knowledge-base/${props.knowledgeBaseId ?? ''}`,
        ],
        actions: ['bedrock:Retrieve'],
      })
    );

    const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [props.userPool],
    });

    const commonAuthorizerProps = {
      authorizationType: AuthorizationType.COGNITO,
      authorizer,
    };
    const ragResource = props.api.root.addResource('rag-knowledge-base');

    // POST: /rag-knowledge-base/retrieve
    const retrieveResource = ragResource.addResource('retrieve');
    retrieveResource.addMethod(
      'POST',
      new LambdaIntegration(retrieveFunction),
      commonAuthorizerProps
    );
  }
}

この中では、ナレッジベースから情報を取得する API を生成しています。

  • NodejsFunctionRetrieve 関数
    • ランタイムは NodeJS の最新版
    • ソースコードはpackages/cdk/lambda/retrieveKnowledgeBase.ts
    • タイムアウトは 15 分
    • 環境変数
      • KNOWLEDGE_BASE_ID: 以下が設定されている場合、設定 (上が優先)
        • スタック作成時のパラメータ ragKnowledgeBaseId (デフォルト値は null)
        • RagKnowledgeBaseStack を作成している場合、その knowledgeBaseId を設定
      • MODEL_REGION: スタック作成時のパラメータ modelRegion (デフォルト値は us-east-1)
    • ロール
      • ナレッジベースリソースに対する bedrock:Retrieve を許可
    • 処理概要
      • ナレッジベースに対して取得コマンドを実行
        • knowledgeBaseId: 環境変数の KNOWLEDGE_BASE_ID
        • retrievalQuery.text: event.body.query
        • retrievalConfiguration.vectorSearchConfiguration:
          • numberOfResults: 固定値 10 (取得するソースチャンクの数)
          • overrideSearchType 固定値 'HYBRID' (ベクター埋め込みと生のテキストの両方を使用する HYBRID 検索)
      • 戻り値に実行結果を設定して終了
  • Api リソースの API エンドポイントに上記関数の呼び出しを追加する
    • API GW オーソライザに Auth リソースの Cognito ユーザプールを指定
    • /rag-knowledge-base/retrieve (POST) => Retrieve Lambda 関数を呼び出し

これで RagKnowledgeBase リソースの作成は完了です。

加えて、GenerativeAiUseCasesStack からallowDownloadFile関数 を呼び出し、ナレッジベースのデータソース (S3 バケット) 内のファイルの署名付き URL を作成できるようにしています。

GenerativeAiUseCasesStack > UseCaseBuilder リソース

UseCaseBuilder リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

image.png

UseCaseBuilder は 生成 AI アプリケーションのユースケースを公開、利用できる機能です。
以下のソースコードが UseCaseBuilder の定義です。

packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
    // Usecase builder
    if (params.useCaseBuilderEnabled) {
      new UseCaseBuilder(this, 'UseCaseBuilder', {
        userPool: auth.userPool,
        api: api.api,
      });
    }

UseCaseBuilder リソースの実体は packages/cdk/lib/construct/use-case-builder.ts にあります。
スタック作成時のパラメータ useCaseBuilderEnabled (デフォルト値は true) が true の場合、以下のコードを実行し UseCaseBuilder を作成します。

packages/cdk/lib/construct/use-case-builder.ts
import {
  RestApi,
  LambdaIntegration,
  CognitoUserPoolsAuthorizer,
  AuthorizationType,
} from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';
import {
  NodejsFunction,
  NodejsFunctionProps,
} from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { Duration } from 'aws-cdk-lib';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import * as ddb from 'aws-cdk-lib/aws-dynamodb';

export interface UseCaseBuilderProps {
  userPool: UserPool;
  api: RestApi;
}
export class UseCaseBuilder extends Construct {
  constructor(scope: Construct, id: string, props: UseCaseBuilderProps) {
    super(scope, id);

    const { userPool, api } = props;

    const useCaseIdIndexName = 'UseCaseIdIndexName';
    const useCaseBuilderTable = new ddb.Table(this, 'UseCaseBuilderTable', {
      partitionKey: {
        name: 'id',
        type: ddb.AttributeType.STRING,
      },
      sortKey: {
        name: 'dataType',
        type: ddb.AttributeType.STRING,
      },
      billingMode: ddb.BillingMode.PAY_PER_REQUEST,
    });

    useCaseBuilderTable.addGlobalSecondaryIndex({
      indexName: useCaseIdIndexName,
      partitionKey: {
        name: 'useCaseId',
        type: ddb.AttributeType.STRING,
      },
      sortKey: {
        name: 'dataType',
        type: ddb.AttributeType.STRING,
      },
      projectionType: ddb.ProjectionType.ALL,
    });

    const commonProperty: NodejsFunctionProps = {
      runtime: Runtime.NODEJS_LATEST,
      timeout: Duration.minutes(15),
      environment: {
        USECASE_TABLE_NAME: useCaseBuilderTable.tableName,
        USECASE_ID_INDEX_NAME: useCaseIdIndexName,
      },
    };

    const commonPath = './lambda/useCaseBuilder';

    // UseCaseBuilder 関連の API を追加する
    const listUseCasesFunction = new NodejsFunction(this, 'ListUseCases', {
      ...commonProperty,
      entry: `${commonPath}/listUseCases.ts`,
    });
    useCaseBuilderTable.grantReadData(listUseCasesFunction);

    const listFavoriteUseCasesFunction = new NodejsFunction(
      this,
      'ListFavoriteUseCases',
      {
        ...commonProperty,
        entry: `${commonPath}/listFavoriteUseCases.ts`,
        environment: {
          ...commonProperty.environment,
          USECASE_ID_INDEX_NAME: useCaseIdIndexName,
        },
      }
    );
    useCaseBuilderTable.grantReadData(listFavoriteUseCasesFunction);

    const getUseCaseFunction = new NodejsFunction(this, 'GetUseCase', {
      ...commonProperty,
      entry: `${commonPath}/getUseCase.ts`,
    });
    useCaseBuilderTable.grantReadData(getUseCaseFunction);

    const createUseCaseFunction = new NodejsFunction(this, 'CreateUseCase', {
      ...commonProperty,
      entry: `${commonPath}/createUseCase.ts`,
    });
    useCaseBuilderTable.grantWriteData(createUseCaseFunction);

    const updateUseCaseFunction = new NodejsFunction(this, 'UpdateUseCase', {
      ...commonProperty,
      entry: `${commonPath}/updateUseCase.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(updateUseCaseFunction);

    const deleteUseCaseFunction = new NodejsFunction(this, 'DeleteUseCase', {
      ...commonProperty,
      entry: `${commonPath}/deleteUseCase.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(deleteUseCaseFunction);

    const toggleFavoriteFunction = new NodejsFunction(this, 'ToggleFavorite', {
      ...commonProperty,
      entry: `${commonPath}/toggleFavorite.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(toggleFavoriteFunction);

    const toggleSharedFunction = new NodejsFunction(this, 'ToggleShared', {
      ...commonProperty,
      entry: `${commonPath}/toggleShared.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(toggleSharedFunction);

    const listRecentlyUsedUseCasesFunction = new NodejsFunction(
      this,
      'ListRecentlyUsedUseCases',
      {
        ...commonProperty,
        entry: `${commonPath}/listRecentlyUsedUseCases.ts`,
      }
    );
    useCaseBuilderTable.grantReadData(listRecentlyUsedUseCasesFunction);

    const updateRecentlyUsedUseCaseFunction = new NodejsFunction(
      this,
      'UpdateRecentlyUsedUseCase',
      {
        ...commonProperty,
        entry: `${commonPath}/updateRecentlyUsedUseCase.ts`,
      }
    );
    useCaseBuilderTable.grantReadWriteData(updateRecentlyUsedUseCaseFunction);

    // API Gateway
    const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [userPool],
    });

    const commonAuthorizerProps = {
      authorizationType: AuthorizationType.COGNITO,
      authorizer,
    };
    const useCasesResource = api.root.addResource('usecases');

    // GET: /usecases
    useCasesResource.addMethod(
      'GET',
      new LambdaIntegration(listUseCasesFunction),
      commonAuthorizerProps
    );

    // POST: /usecases
    useCasesResource.addMethod(
      'POST',
      new LambdaIntegration(createUseCaseFunction),
      commonAuthorizerProps
    );

    const favoriteUseCaseResource = useCasesResource.addResource('favorite');

    // GET: /usecases/favorite
    favoriteUseCaseResource.addMethod(
      'GET',
      new LambdaIntegration(listFavoriteUseCasesFunction),
      commonAuthorizerProps
    );

    const useCaseResource = useCasesResource.addResource('{useCaseId}');

    // GET: /usecases/{useCaseId}
    useCaseResource.addMethod(
      'GET',
      new LambdaIntegration(getUseCaseFunction),
      commonAuthorizerProps
    );

    // PUT: /usecases/{useCaseId}
    useCaseResource.addMethod(
      'PUT',
      new LambdaIntegration(updateUseCaseFunction),
      commonAuthorizerProps
    );

    // DELETE: /usecases/{useCaseId}
    useCaseResource.addMethod(
      'DELETE',
      new LambdaIntegration(deleteUseCaseFunction),
      commonAuthorizerProps
    );

    const favoriteResource = useCaseResource.addResource('favorite');

    // PUT: /usecases/{useCaseId}/favorite
    favoriteResource.addMethod(
      'PUT',
      new LambdaIntegration(toggleFavoriteFunction),
      commonAuthorizerProps
    );

    const sharedResource = useCaseResource.addResource('shared');

    // PUT: /usecases/{useCaseId}/shared
    sharedResource.addMethod(
      'PUT',
      new LambdaIntegration(toggleSharedFunction),
      commonAuthorizerProps
    );

    const recentUseCasesResource = useCasesResource.addResource('recent');

    // GET: /usecases/recent
    recentUseCasesResource.addMethod(
      'GET',
      new LambdaIntegration(listRecentlyUsedUseCasesFunction),
      commonAuthorizerProps
    );

    const recentUseCaseResource =
      recentUseCasesResource.addResource('{useCaseId}');

    // PUT: /usecases/recent/{useCaseId}
    recentUseCaseResource.addMethod(
      'PUT',
      new LambdaIntegration(updateRecentlyUsedUseCaseFunction),
      commonAuthorizerProps
    );
  }
}

この中では、ナレッジベースから情報を取得する API を生成しています。

GenerativeAiUseCasesStack > UseCaseBuilder > Table リソース

Table はユースケース用の DynamoDB テーブルのリソースです。

以下のソースコードが Table の定義です。

packages/cdk/lib/construct/transcribe.ts (抜粋)
    const useCaseIdIndexName = 'UseCaseIdIndexName';
    const useCaseBuilderTable = new ddb.Table(this, 'UseCaseBuilderTable', {
      partitionKey: {
        name: 'id',
        type: ddb.AttributeType.STRING,
      },
      sortKey: {
        name: 'dataType',
        type: ddb.AttributeType.STRING,
      },
      billingMode: ddb.BillingMode.PAY_PER_REQUEST,
    });

    useCaseBuilderTable.addGlobalSecondaryIndex({
      indexName: useCaseIdIndexName,
      partitionKey: {
        name: 'useCaseId',
        type: ddb.AttributeType.STRING,
      },
      sortKey: {
        name: 'dataType',
        type: ddb.AttributeType.STRING,
      },
      projectionType: ddb.ProjectionType.ALL,
    });

ここでは、以下のリソースを生成しています。

GenerativeAiUseCasesStack > UseCaseBuilder > NodejsFunction リソース

NodejsFunction はユースケース用の Lambda 関数のリソースです。

以下のソースコードが NodejsFunction の定義です。

packages/cdk/lib/construct/transcribe.ts (抜粋)

    const commonProperty: NodejsFunctionProps = {
      runtime: Runtime.NODEJS_LATEST,
      timeout: Duration.minutes(15),
      environment: {
        USECASE_TABLE_NAME: useCaseBuilderTable.tableName,
        USECASE_ID_INDEX_NAME: useCaseIdIndexName,
      },
    };

    const commonPath = './lambda/useCaseBuilder';

    // UseCaseBuilder 関連の API を追加する
    const listUseCasesFunction = new NodejsFunction(this, 'ListUseCases', {
      ...commonProperty,
      entry: `${commonPath}/listUseCases.ts`,
    });
    useCaseBuilderTable.grantReadData(listUseCasesFunction);

    const listFavoriteUseCasesFunction = new NodejsFunction(
      this,
      'ListFavoriteUseCases',
      {
        ...commonProperty,
        entry: `${commonPath}/listFavoriteUseCases.ts`,
        environment: {
          ...commonProperty.environment,
          USECASE_ID_INDEX_NAME: useCaseIdIndexName,
        },
      }
    );
    useCaseBuilderTable.grantReadData(listFavoriteUseCasesFunction);

    const getUseCaseFunction = new NodejsFunction(this, 'GetUseCase', {
      ...commonProperty,
      entry: `${commonPath}/getUseCase.ts`,
    });
    useCaseBuilderTable.grantReadData(getUseCaseFunction);

    const createUseCaseFunction = new NodejsFunction(this, 'CreateUseCase', {
      ...commonProperty,
      entry: `${commonPath}/createUseCase.ts`,
    });
    useCaseBuilderTable.grantWriteData(createUseCaseFunction);

    const updateUseCaseFunction = new NodejsFunction(this, 'UpdateUseCase', {
      ...commonProperty,
      entry: `${commonPath}/updateUseCase.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(updateUseCaseFunction);

    const deleteUseCaseFunction = new NodejsFunction(this, 'DeleteUseCase', {
      ...commonProperty,
      entry: `${commonPath}/deleteUseCase.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(deleteUseCaseFunction);

    const toggleFavoriteFunction = new NodejsFunction(this, 'ToggleFavorite', {
      ...commonProperty,
      entry: `${commonPath}/toggleFavorite.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(toggleFavoriteFunction);

    const toggleSharedFunction = new NodejsFunction(this, 'ToggleShared', {
      ...commonProperty,
      entry: `${commonPath}/toggleShared.ts`,
    });
    useCaseBuilderTable.grantReadWriteData(toggleSharedFunction);

    const listRecentlyUsedUseCasesFunction = new NodejsFunction(
      this,
      'ListRecentlyUsedUseCases',
      {
        ...commonProperty,
        entry: `${commonPath}/listRecentlyUsedUseCases.ts`,
      }
    );
    useCaseBuilderTable.grantReadData(listRecentlyUsedUseCasesFunction);

    const updateRecentlyUsedUseCaseFunction = new NodejsFunction(
      this,
      'UpdateRecentlyUsedUseCase',
      {
        ...commonProperty,
        entry: `${commonPath}/updateRecentlyUsedUseCase.ts`,
      }
    );
    useCaseBuilderTable.grantReadWriteData(updateRecentlyUsedUseCaseFunction);

ここでは、以下のリソースを生成しています。

  • 共通設定
    • ランタイムは NodeJS の最新版
    • タイムアウトは 15 分
    • 環境変数
      • USECASE_TABLE_NAME: DynamoDB テーブル UseCaseBuilderTable のテーブル名
      • USECASE_ID_INDEX_NAME: GSI (グローバルセカンダリインデックス) UseCaseIdIndexName のインデックス名
  • ListUseCases 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/listUseCases.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの listUseCases 関数を呼び出して DynamoDB テーブルからデータを取得
        • Cognito ユーザに紐づくユースケース一覧を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: useCase で始まる
          • 取得件数 Limit: 30 件 (マイユースケースのページあたりの件数)
          • ExclusiveStartKey: event.queryStringParameters.exclusiveStartKey があればそのキーから取得する
        • Cognito ユーザに紐づくお気に入りを全取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: favorite で始まる
      • 戻り値に取得したデータを設定
  • ListFavoriteUseCases 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/listFavoriteUseCases.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの listFavoriteUseCases 関数を呼び出して DynamoDB テーブルからデータを取得
        • Cognito ユーザに紐づくお気に入り一覧を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: favorite で始まる
          • 取得件数 Limit: 20 件 (お気に入りのページあたりの件数)
          • ExclusiveStartKey: クエリパラメータ文字列の exclusiveStartKey があればそのキーから取得する
        • 取得したユースケース一覧の情報を繰り返し取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • IndexName: ユースケースビルダー用 DynamnoDB テーブル GSI 名
          • useCaseId: 取得したユースケース ID
          • dataType: useCase で始まる
        • 自分が作成したユースケースもしくはシェアされているユースケースに絞る (後から非公開となったユースケースを除外)
      • 戻り値に取得したデータを設定
  • GetUseCase 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/getUseCase.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの getUseCase 関数を呼び出して DynamoDB テーブルからデータを取得
        • 指定したユースケースの情報を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • IndexName: ユースケースビルダー用 DynamnoDB テーブル GSI 名
          • useCaseId: パスパラメータの useCaseId
          • dataType: useCase で始まる
        • 自分が作成したユースケースもしくはシェアされているユースケースでない場合、null にする (後から非公開となったユースケースを除外)
      • 戻り値に取得したデータを設定
  • CreateUseCase 関数
  • UpdateUseCase 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/updateUseCase.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの updateUseCase 関数を呼び出して DynamoDB テーブルのデータを更新
        • 指定したユースケースの情報を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • IndexName: ユースケースビルダー用 DynamnoDB テーブル GSI 名
          • useCaseId: パスパラメータの useCaseId
          • dataType: useCase で始まる
        • 存在チェックおよび自身のユースケースかをチェック
        • ユースケースの情報を更新
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: 取得した id
          • dataType: 取得した dataType
          • title: event.body.title
          • description: event.body.description
          • promptTemplate: event.body.promptTemplate
          • inputExamples: event.body.inputExamples
          • fixedModelId: event.body.fixedModelId
          • fileUpload: event.body.fileUpload
  • DeleteUseCase 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/deleteUseCase.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの deleteUseCase 関数を呼び出して DynamoDB テーブルのデータを削除
        • 指定したユースケースの情報を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • IndexName: ユースケースビルダー用 DynamnoDB テーブル GSI 名
          • useCaseId: パスパラメータの useCaseId
          • dataType: useCase で始まる
        • 存在チェックおよび自身のユースケースかをチェック
        • 指定したユースケースの全情報を取得 (dataType で絞り込まない)
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • IndexName: ユースケースビルダー用 DynamnoDB テーブル GSI 名
          • useCaseId: パスパラメータの useCaseId
        • 取得したユースケースを削除
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: 取得した id
          • dataType: useCase# + 現在日時のミリ秒
  • ToggleFavorite 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/toggleFavorite.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの toggleFavorite 関数を呼び出して DynamoDB テーブルのデータを更新
        • 指定したユースケースの情報を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • useCaseId: パスパラメータの useCaseId
          • dataType: favorite で始まる
        • ユースケースの情報が取得できた場合、お気に入りを解除 (データを削除)
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: 取得した id
          • dataType: 取得した dataType
        • ユースケースの情報が取得できなかった場合、お気に入りを登録
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: favorite# + 現在日時のミリ秒
          • useCaseId: 取得した useCaseId
  • ToggleShared 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/toggleShared.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの toggleShared 関数を呼び出して DynamoDB テーブルのデータを更新
        • 指定したユースケースの情報を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • useCaseId: パスパラメータの useCaseId
          • dataType: favorite で始まる
        • 存在チェックおよび自身のユースケースかをチェックし、自身のものでない場合は isShared: false を返却
        • ユースケースの情報が取得できなかった場合、お気に入りを登録
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: 取得した id
          • dataType: 取得した dataType
          • isShared: 取得した isShared を反転して登録
  • ListRecentlyUsedUseCases 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/listRecentlyUsedUseCases.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの listRecentlyUsedUseCases 関数を呼び出して DynamoDB テーブルからデータを取得
        • Cognito ユーザに紐づく利用履歴一覧を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: recentlyUsed で始まる
          • 取得件数 Limit: 20 件 (利用履歴のページあたりの件数)
          • ExclusiveStartKey: クエリパラメータ文字列の exclusiveStartKey があればそのキーから取得する
        • 取得したユースケースの情報を取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • IndexName: ユースケースビルダー用 DynamnoDB テーブル GSI 名
          • useCaseId: パスパラメータの useCaseId
          • dataType: useCase で始まる
        • Cognito ユーザに紐づくお気に入りを全取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: favorite で始まる
      • 戻り値に取得したデータを設定
  • UpdateRecentlyUsedUseCase 関数
    • ソースコードはpackages/cdk/lambda/useCaseBuilder/updateRecentlyUsedUseCase.ts
    • 処理概要
      • Cognito ユーザ ID を取得
      • packages/cdk/lambda/useCaseBuilder/useCaseBuilderRepository.tsの updateRecentlyUsedUseCase 関数を呼び出して DynamoDB テーブルのデータを更新
        • Cognito ユーザに紐づく利用履歴を全取得
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: recentlyUsed で始まる
        • 既に利用履歴がリミット (100 件) を超えている場合、利用履歴を 100 件にスライス
        • 利用履歴への登録対象のユースケースが既に利用履歴に含まれている場合、削除を行う
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: 取得した id
          • dataType: 取得した dataType
        • 利用履歴への登録を行う
          • TableName: ユースケースビルダー用 DynamnoDB テーブル名
          • id: useCase# + Cognito ユーザ ID
          • dataType: recentlyUsed# + 現在日時のミリ秒
          • useCaseId: パスパラメータの useCaseId
GenerativeAiUseCasesStack > UseCaseBuilder > Lambda 関数を API に追加

続いて、Lambda 関数を Api リソースの API エンドポイントに追加します。

packages/cdk/lib/construct/transcribe.ts (抜粋)
    // API Gateway
    const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [userPool],
    });

    const commonAuthorizerProps = {
      authorizationType: AuthorizationType.COGNITO,
      authorizer,
    };
    const useCasesResource = api.root.addResource('usecases');

    // GET: /usecases
    useCasesResource.addMethod(
      'GET',
      new LambdaIntegration(listUseCasesFunction),
      commonAuthorizerProps
    );

    // POST: /usecases
    useCasesResource.addMethod(
      'POST',
      new LambdaIntegration(createUseCaseFunction),
      commonAuthorizerProps
    );

    const favoriteUseCaseResource = useCasesResource.addResource('favorite');

    // GET: /usecases/favorite
    favoriteUseCaseResource.addMethod(
      'GET',
      new LambdaIntegration(listFavoriteUseCasesFunction),
      commonAuthorizerProps
    );

    const useCaseResource = useCasesResource.addResource('{useCaseId}');

    // GET: /usecases/{useCaseId}
    useCaseResource.addMethod(
      'GET',
      new LambdaIntegration(getUseCaseFunction),
      commonAuthorizerProps
    );

    // PUT: /usecases/{useCaseId}
    useCaseResource.addMethod(
      'PUT',
      new LambdaIntegration(updateUseCaseFunction),
      commonAuthorizerProps
    );

    // DELETE: /usecases/{useCaseId}
    useCaseResource.addMethod(
      'DELETE',
      new LambdaIntegration(deleteUseCaseFunction),
      commonAuthorizerProps
    );

    const favoriteResource = useCaseResource.addResource('favorite');

    // PUT: /usecases/{useCaseId}/favorite
    favoriteResource.addMethod(
      'PUT',
      new LambdaIntegration(toggleFavoriteFunction),
      commonAuthorizerProps
    );

    const sharedResource = useCaseResource.addResource('shared');

    // PUT: /usecases/{useCaseId}/shared
    sharedResource.addMethod(
      'PUT',
      new LambdaIntegration(toggleSharedFunction),
      commonAuthorizerProps
    );

    const recentUseCasesResource = useCasesResource.addResource('recent');

    // GET: /usecases/recent
    recentUseCasesResource.addMethod(
      'GET',
      new LambdaIntegration(listRecentlyUsedUseCasesFunction),
      commonAuthorizerProps
    );

    const recentUseCaseResource =
      recentUseCasesResource.addResource('{useCaseId}');

    // PUT: /usecases/recent/{useCaseId}
    recentUseCaseResource.addMethod(
      'PUT',
      new LambdaIntegration(updateRecentlyUsedUseCaseFunction),
      commonAuthorizerProps
    );

ここでは、Api リソースの API エンドポイントに上記関数の呼び出しを追加しています。

  • API GW オーソライザに Auth リソースの Cognito ユーザプールを指定
  • /usecases (GET) => ListUseCases Lambda 関数を呼び出し
  • /usecases/favorite (GET) => ListFavoriteUseCases Lambda 関数を呼び出し
  • /usecases/{useCaseId} (GET) => GetUseCase Lambda 関数を呼び出し
  • /usecases (POST) => CreateUseCase Lambda 関数を呼び出し
  • /usecases/{useCaseId} (PUT) => UpdateUseCase Lambda 関数を呼び出し
  • /usecases/{useCaseId} (DELETE) => DeleteUseCase Lambda 関数を呼び出し
  • /usecases/{useCaseId}/favorite (PUT) => ToggleFavorite Lambda 関数を呼び出し
  • /usecases/{useCaseId}/shared (PUT) => ToggleShared Lambda 関数を呼び出し
  • /usecases/recent (GET) => ListRecentlyUsedUseCases Lambda 関数を呼び出し
  • /usecases/recent/{useCaseId} (PUT) => UpdateRecentlyUsedUseCase Lambda 関数を呼び出し

GenerativeAiUseCasesStack > Transcribe リソース

Transcribe リソースは、アーキテクチャ図でいうと以下の赤枠の部分にあたります。

image.png

Transcribe は音声データの文字起こし機能です。
以下のソースコードが Transcribe の定義です。

packages/cdk/lib/generative-ai-use-cases-stack.ts (抜粋)
    // Transcribe
    new Transcribe(this, 'Transcribe', {
      userPool: auth.userPool,
      idPool: auth.idPool,
      api: api.api,
    });

Transcribe リソースの実体は packages/cdk/lib/construct/transcribe.ts にあります。

packages/cdk/lib/construct/transcribe.ts
import { Duration, RemovalPolicy } from 'aws-cdk-lib';
import {
  AuthorizationType,
  CognitoUserPoolsAuthorizer,
  LambdaIntegration,
  RestApi,
} from 'aws-cdk-lib/aws-apigateway';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import { IdentityPool } from '@aws-cdk/aws-cognito-identitypool-alpha';
import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import {
  BlockPublicAccess,
  Bucket,
  BucketEncryption,
  HttpMethods,
} from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export interface TranscribeProps {
  userPool: UserPool;
  idPool: IdentityPool;
  api: RestApi;
}

export class Transcribe extends Construct {
  constructor(scope: Construct, id: string, props: TranscribeProps) {
    super(scope, id);

    const audioBucket = new Bucket(this, 'AudioBucket', {
      encryption: BucketEncryption.S3_MANAGED,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      enforceSSL: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    });
    audioBucket.addCorsRule({
      allowedOrigins: ['*'],
      allowedMethods: [HttpMethods.PUT],
      allowedHeaders: ['*'],
      exposedHeaders: [],
      maxAge: 3000,
    });

    const transcriptBucket = new Bucket(this, 'TranscriptBucket', {
      encryption: BucketEncryption.S3_MANAGED,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      enforceSSL: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    });

    const getSignedUrlFunction = new NodejsFunction(this, 'GetSignedUrl', {
      runtime: Runtime.NODEJS_LATEST,
      entry: './lambda/getFileUploadSignedUrl.ts',
      timeout: Duration.minutes(15),
      environment: {
        BUCKET_NAME: audioBucket.bucketName,
      },
    });
    audioBucket.grantWrite(getSignedUrlFunction);

    const startTranscriptionFunction = new NodejsFunction(
      this,
      'StartTranscription',
      {
        runtime: Runtime.NODEJS_LATEST,
        entry: './lambda/startTranscription.ts',
        timeout: Duration.minutes(15),
        environment: {
          TRANSCRIPT_BUCKET_NAME: transcriptBucket.bucketName,
        },
        initialPolicy: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['transcribe:*'],
            resources: ['*'],
          }),
        ],
      }
    );
    audioBucket.grantRead(startTranscriptionFunction);
    transcriptBucket.grantWrite(startTranscriptionFunction);

    const getTranscriptionFunction = new NodejsFunction(
      this,
      'GetTranscription',
      {
        runtime: Runtime.NODEJS_LATEST,
        entry: './lambda/getTranscription.ts',
        timeout: Duration.minutes(15),
        initialPolicy: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['transcribe:*'],
            resources: ['*'],
          }),
        ],
      }
    );
    transcriptBucket.grantRead(getTranscriptionFunction);

    // API Gateway
    const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [props.userPool],
    });

    const commonAuthorizerProps = {
      authorizationType: AuthorizationType.COGNITO,
      authorizer,
    };
    const transcribeResource = props.api.root.addResource('transcribe');

    // POST: /transcribe/start
    transcribeResource
      .addResource('start')
      .addMethod(
        'POST',
        new LambdaIntegration(startTranscriptionFunction),
        commonAuthorizerProps
      );

    // POST: /transcribe/url
    transcribeResource
      .addResource('url')
      .addMethod(
        'POST',
        new LambdaIntegration(getSignedUrlFunction),
        commonAuthorizerProps
      );

    // GET: /transcribe/result/{jobName}
    transcribeResource
      .addResource('result')
      .addResource('{jobName}')
      .addMethod(
        'GET',
        new LambdaIntegration(getTranscriptionFunction),
        commonAuthorizerProps
      );

    // add Policy for Amplify User
    // grant access policy transcribe stream and translate
    props.idPool.authenticatedRole.attachInlinePolicy(
      new Policy(this, 'GrantAccessTranscribeStream', {
        statements: [
          new PolicyStatement({
            actions: ['transcribe:StartStreamTranscriptionWebSocket'],
            resources: ['*'],
          }),
        ],
      })
    );
  }
}

この中では、元となる音声データ用のバケットや、文字起こし処理および API エンドポイント、文字起こし結果を格納する S3 バケットを生成しています。

GenerativeAiUseCasesStack > Transcribe > Bucket リソース

Bucket は文字起こし用の S3 バケットのリソースです。

以下のソースコードが Bucket の定義です。

packages/cdk/lib/construct/transcribe.ts (抜粋)
    const audioBucket = new Bucket(this, 'AudioBucket', {
      encryption: BucketEncryption.S3_MANAGED,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      enforceSSL: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    });
    audioBucket.addCorsRule({
      allowedOrigins: ['*'],
      allowedMethods: [HttpMethods.PUT],
      allowedHeaders: ['*'],
      exposedHeaders: [],
      maxAge: 3000,
    });

    const transcriptBucket = new Bucket(this, 'TranscriptBucket', {
      encryption: BucketEncryption.S3_MANAGED,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      enforceSSL: true,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
    });

ここでは、以下のリソースを生成しています。

  • AudioBucket S3 バケット
    • S3 マネージドキーでの暗号化
    • スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
    • SSL 接続を強制する
    • ブロックパブリックアクセスはブロック
    • CORS 設定を許可する
      • すべてのオリジン
      • PUT リクエストを許可
      • すべての Access-Control-Request-Headers を許可
      • アクセスできるヘッダ情報はなし
      • ブラウザのキャッシュ時間は 50 分(3000 秒)
  • TranscriptBucket S3 バケット
    • S3 マネージドキーでの暗号化
    • スタック削除時はバケットを削除 (中に含まれるオブジェクトも削除)
    • SSL 接続を強制する
    • ブロックパブリックアクセスはブロック
GenerativeAiUseCasesStack > Transcribe > NodejsFunction リソース

NodejsFunction は文字起こし用の Lambda 関数です。

packages/cdk/lib/construct/transcribe.ts (抜粋)
    const getSignedUrlFunction = new NodejsFunction(this, 'GetSignedUrl', {
      runtime: Runtime.NODEJS_LATEST,
      entry: './lambda/getFileUploadSignedUrl.ts',
      timeout: Duration.minutes(15),
      environment: {
        BUCKET_NAME: audioBucket.bucketName,
      },
    });
    audioBucket.grantWrite(getSignedUrlFunction);

    const startTranscriptionFunction = new NodejsFunction(
      this,
      'StartTranscription',
      {
        runtime: Runtime.NODEJS_LATEST,
        entry: './lambda/startTranscription.ts',
        timeout: Duration.minutes(15),
        environment: {
          TRANSCRIPT_BUCKET_NAME: transcriptBucket.bucketName,
        },
        initialPolicy: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['transcribe:*'],
            resources: ['*'],
          }),
        ],
      }
    );
    audioBucket.grantRead(startTranscriptionFunction);
    transcriptBucket.grantWrite(startTranscriptionFunction);

    const getTranscriptionFunction = new NodejsFunction(
      this,
      'GetTranscription',
      {
        runtime: Runtime.NODEJS_LATEST,
        entry: './lambda/getTranscription.ts',
        timeout: Duration.minutes(15),
        initialPolicy: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['transcribe:*'],
            resources: ['*'],
          }),
        ],
      }
    );
    transcriptBucket.grantRead(getTranscriptionFunction);

ここでは、以下のリソースを生成しています。

  • GetSignedUrl 関数
  • StartTranscription 関数
    • ランタイムは NodeJS の最新版
    • ソースコードはpackages/cdk/lambda/startTranscription.ts
    • タイムアウトは 15 分
    • 環境変数
      • TRANSCRIPT_BUCKET_NAME: TranscriptBucket S3 バケットのバケット名
    • ロール
      • 全リソースへの transcribe:* アクションを許可
    • 処理概要
      • 文字起こしジョブの開始コマンドを作成する
        • IdentifyLanguage: 固定値 true
        • LanguageOptions: 固定値 ['ja-JP', 'en-US']
        • Media:
          • MediaFileUri: event.body.audioUrl
        • TranscriptionJobName: ランダム ID,
        • Settings:
          • ShowSpeakerLabels: event.body.speakerLabel
          • MaxSpeakerLabels: event.body.speakerLabel が設定されていれば event.body.maxSpeakers を設定
        • OutputBucketName: 環境変数の TRANSCRIPT_BUCKET_NAME
        • Tags:
          • Key: 固定値 'userId'
          • Value: オーソライザーの sub (ユーザごとのランダム ID) 値
      • コマンドを実行する
      • 文字起こしジョブ名を返却する
  • GetTranscription 関数
    • ランタイムは NodeJS の最新版
    • ソースコードはpackages/cdk/lambda/getTranscription.ts
    • タイムアウトは 15 分
    • ロール
      • 全リソースへの transcribe:* アクションを許可
    • 処理概要
      • 文字起こしジョブの情報取得コマンドを作成する
        • TranscriptionJobName: パスパラメータの jobName
      • コマンドを実行する
      • 取得したジョブ情報の Tag.userId がログインユーザと異なる場合は 403 エラーを返却
      • ジョブステータスが COMPLETED の場合
        • ジョブ情報の TranscriptFileUri から 文字起こし結果が格納されている S3 バケット名とキー値を取得する
        • S3 バケットからオブジェクトを取得する
        • 文字起こし結果をフォーマットして返却
      • ジョブステータスが COMPLETED 以外の場合
        • ジョブステータスを返却する
GenerativeAiUseCasesStack > Transcribe > Lambda 関数を API に追加

続いて、Lambda 関数を Api リソースの API エンドポイントに追加します。

packages/cdk/lib/construct/transcribe.ts (抜粋)
    // API Gateway
    const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', {
      cognitoUserPools: [props.userPool],
    });

    const commonAuthorizerProps = {
      authorizationType: AuthorizationType.COGNITO,
      authorizer,
    };
    const transcribeResource = props.api.root.addResource('transcribe');

    // POST: /transcribe/start
    transcribeResource
      .addResource('start')
      .addMethod(
        'POST',
        new LambdaIntegration(startTranscriptionFunction),
        commonAuthorizerProps
      );

    // POST: /transcribe/url
    transcribeResource
      .addResource('url')
      .addMethod(
        'POST',
        new LambdaIntegration(getSignedUrlFunction),
        commonAuthorizerProps
      );

    // GET: /transcribe/result/{jobName}
    transcribeResource
      .addResource('result')
      .addResource('{jobName}')
      .addMethod(
        'GET',
        new LambdaIntegration(getTranscriptionFunction),
        commonAuthorizerProps
      );

    // add Policy for Amplify User
    // grant access policy transcribe stream and translate
    props.idPool.authenticatedRole.attachInlinePolicy(
      new Policy(this, 'GrantAccessTranscribeStream', {
        statements: [
          new PolicyStatement({
            actions: ['transcribe:StartStreamTranscriptionWebSocket'],
            resources: ['*'],
          }),
        ],
      })
    );

ここでは、以下のリソースを生成しています。

  • Api リソースの API エンドポイントに上記関数の呼び出しを追加する
    • API GW オーソライザに Auth リソースの Cognito ユーザプールを指定
    • /transcribe/url (POST) => GetSignedUrl Lambda 関数を呼び出し
    • /transcribe/start (POST) => StartTranscription Lambda 関数を呼び出し
    • /transcribe/result/{jobName} (Get) => GetTranscription Lambda 関数を呼び出し
GenerativeAiUseCasesStack > Transcribe > Cognito 認証済ユーザへの権限追加

最後に、Cognito 認証済ユーザにトランスクリプト権限を追加します。

packages/cdk/lib/construct/transcribe.ts (抜粋)
    // add Policy for Amplify User
    // grant access policy transcribe stream and translate
    props.idPool.authenticatedRole.attachInlinePolicy(
      new Policy(this, 'GrantAccessTranscribeStream', {
        statements: [
          new PolicyStatement({
            actions: ['transcribe:StartStreamTranscriptionWebSocket'],
            resources: ['*'],
          }),
        ],
      })
    );

ここでは、Cognito 認証済ユーザにトランスクリプト権限を追加しています。

  • GrantAccessTranscribeStream ポリシー
    • すべてのリソースに対し transcribe:StartStreamTranscriptionWebSocket を許可

以上で RagKnowledgeBase, UseCaseBuilder, Transcribe リソースの解説は終了です。
これにて、GenerativeAiUseCasesStack スタックの解説が全て終わりました。
次回は最後のスタックである、GenU 内の DashboardStack スタックを解説したいと思います。

(参考) GenU のバックエンド (CDK) 詳細解説投稿一覧

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?