├── .github ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE └── node.js.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── THIRD_PARTY_LICENSE ├── docs ├── CDKMigration.md ├── DeveloperGuide.md └── png │ ├── arch.drawio.png │ ├── feature1.png │ ├── feature2.png │ ├── feature3.png │ ├── flow-ai.png │ ├── flow-kendra.png │ ├── flow-rag.png │ ├── kendra-frow.png │ ├── layout.png │ ├── rag-screenshot.png │ └── search-flow.png ├── package-lock.json ├── package.json ├── packages ├── cdk │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── bin │ │ └── jp-rag-sample.ts │ ├── cdk.json │ ├── lambda │ │ ├── checkEmailDomain.ts │ │ ├── kendra.ts │ │ ├── kendraSync.ts │ │ ├── predictStream.ts │ │ └── utils │ │ │ ├── api.ts │ │ │ ├── bedrockApi.ts │ │ │ └── models.ts │ ├── lib │ │ ├── cloud-front-waf-stack.ts │ │ ├── construct │ │ │ ├── api.ts │ │ │ ├── auth.ts │ │ │ ├── common-web-acl.ts │ │ │ ├── index.ts │ │ │ ├── rag.ts │ │ │ └── web.ts │ │ └── jp-rag-sample-stack.ts │ ├── package.json │ └── tsconfig.json ├── types │ ├── package.json │ └── src │ │ ├── index.d.ts │ │ ├── message.d.ts │ │ ├── protocol.d.ts │ │ ├── text.d.ts │ │ └── utils.d.ts └── web │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ ├── favicon.png │ ├── favicon.svg │ └── images │ │ ├── aws.svg │ │ ├── aws_bg_white.svg │ │ ├── aws_icon_180.png │ │ ├── aws_icon_180_bg_white.png │ │ ├── aws_icon_192.png │ │ ├── aws_icon_192_maskable.png │ │ ├── aws_icon_512.png │ │ ├── aws_icon_512_maskable.png │ │ └── aws_maskable.svg │ ├── src │ ├── App.tsx │ ├── README.md │ ├── components │ │ ├── AuthWithSAML.tsx │ │ └── AuthWithUserpool.tsx │ ├── i18n │ │ ├── configs.ts │ │ ├── en.json │ │ └── ja.json │ ├── layout │ │ ├── AiArea.tsx │ │ ├── FilterArea.tsx │ │ ├── InputWithSuggest.tsx │ │ ├── KendraAreaAssets │ │ │ ├── KendraAreaMain.tsx │ │ │ └── components │ │ │ │ ├── HighlightedTexts.tsx │ │ │ │ ├── KendraResultDoc.tsx │ │ │ │ ├── KendraResultExcerpt.tsx │ │ │ │ ├── KendraResultFAQ.tsx │ │ │ │ └── KendraResultFeatured.tsx │ │ ├── MainArea.tsx │ │ ├── TOTP.tsx │ │ └── TopBar.tsx │ ├── main.tsx │ ├── utils │ │ ├── constant.tsx │ │ ├── function.tsx │ │ ├── globalContext.ts │ │ ├── interface.tsx │ │ ├── service.ts │ │ ├── top_queries.json │ │ └── useGlobalContext.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── setup-env.sh /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## Issue Report 2 | 3 | (A clear and concise description of the issue) 4 | 5 | ## Expected Behavior 6 | 7 | (Write out the expected behavior here.) 8 | 9 | ## Actual Behavior 10 | 11 | (Write out what actually happened here.) 12 | 13 | ## Steps to Reproduce the Issue 14 | 15 | (Write out detailed steps to reproduce the issue here.) -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 9 | 10 | ## Motivation 11 | 12 | (Write your motivation here.) 13 | 14 | ## Proposed Changes 15 | 16 | (Write out the details of your proposed changes here.) 17 | 18 | ## Test Plan 19 | 20 | (Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.) -------------------------------------------------------------------------------- /.github/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js(Web + CDK) CI 5 | 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | check-lint-build: 14 | name: 'Check lint and build' 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run web:lint 31 | - run: npm run cdk:lint 32 | - run: npm run web:build 33 | - name: 'エラー防止のために cdk.json に仮の値を設定' 34 | run: | 35 | cat ./packages/cdk/cdk.json | jq '.context.openAiApiKeySecretArn |= "arn:aws:secretsmanager:us-west-2:123456789012:secret:openai-secret-XXXXXX"' > ./packages/cdk/temp.json 36 | mv ./packages/cdk/temp.json ./packages/cdk/cdk.json 37 | - run: npm -w packages/cdk run cdk synth 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | !.gitkeep -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.json 2 | **/*.md 3 | **/*.txt 4 | **/node_modules 5 | **/dist 6 | 7 | cdk.out 8 | packages/web/dev-dist 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "bracketSpacing": true, 7 | "bracketSameLine": true, 8 | "arrowParens": "always", 9 | "plugins": [ 10 | "prettier-plugin-tailwindcss" 11 | ] 12 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.0.0 4 | 5 | Initial Release 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | このプロジェクトは [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct) を採用しています。 3 | 詳細については、[Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) もしくは 4 | opensource-codeofconduct@amazon.com にコメント下さい。 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # contribute ガイドライン 2 | 私たちのプロジェクトへの contribute に興味を持っていただき、ありがとうございます。バグレポート、新機能、修正、追加のドキュメントなど、コミュニティからのフィードバックや contribute を大いに歓迎しています。 3 | 4 | フィードバックや contribute の前に、このドキュメントを読んで、効果的に対応するために必要な情報を入手してください。 5 | 6 | ## 変更履歴 7 | [CHANGELOG](CHANGELOG.md) を参照して下さい。 8 | 9 | ## バグ・機能追加に関するリクエスト 10 | [SUPPORT](SUPPORT.md) を参照して下さい。 11 | 12 | ## Code of Conduct 13 | [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) を参照して下さい。 14 | 15 | ## セキュリティに関する問題の連絡 16 | [SECURITY](SECURITY.md) を参照して下さい。 17 | 18 | ## Licensing 19 | プロジェクトのライセンスについては、[LICENSE](LICENSE.txt) ファイルを参照してください。 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | department related to name license period material / not material license type link remote version installed version defined version author 2 | ---------- ---------- ---- -------------- ----------------------- ------------ ---- -------------- ----------------- --------------- ------ 3 | kessler stuff @aws-amplify/ui-react perpetual material Apache-2.0 git+https://github.com/aws-amplify/amplify-ui.git 5.0.4 5.0.4 ^5.0.1 n/a 4 | kessler stuff @aws-sdk/client-kendra perpetual material Apache-2.0 git+https://github.com/aws/aws-sdk-js-v3.git 3.369.0 3.363.0 ^3.334.0 AWS SDK for JavaScript Team https://aws.amazon.com/javascript/ 5 | kessler stuff @aws-sdk/client-s3 perpetual material Apache-2.0 git+https://github.com/aws/aws-sdk-js-v3.git 3.369.0 3.367.0 ^3.332.0 AWS SDK for JavaScript Team https://aws.amazon.com/javascript/ 6 | kessler stuff @aws-sdk/s3-request-presigner perpetual material Apache-2.0 git+https://github.com/aws/aws-sdk-js-v3.git 3.369.0 3.367.0 ^3.335.0 AWS SDK for JavaScript Team https://aws.amazon.com/javascript/ 7 | kessler stuff @chakra-ui/icons perpetual material MIT git+https://github.com/chakra-ui/chakra-ui.git 2.0.19 2.0.19 ^2.0.19 Segun Adebayo 8 | kessler stuff @chakra-ui/react perpetual material MIT git+https://github.com/chakra-ui/chakra-ui.git 2.7.1 2.7.1 ^2.6.1 Segun Adebayo 9 | kessler stuff @emotion/react perpetual material MIT git+https://github.com/emotion-js/emotion.git#main 11.11.1 11.11.1 ^11.11.0 Emotion Contributors 10 | kessler stuff @emotion/styled perpetual material MIT git+https://github.com/emotion-js/emotion.git#main 11.11.0 11.11.0 ^11.11.0 n/a 11 | kessler stuff @types/node perpetual material MIT https://github.com/DefinitelyTyped/DefinitelyTyped.git 20.4.1 20.4.1 ^20.1.7 n/a 12 | kessler stuff aws-amplify perpetual material Apache-2.0 git+https://github.com/aws-amplify/amplify-js.git 5.3.3 5.3.3 ^5.2.5 Amazon Web Services 13 | kessler stuff framer-motion perpetual material MIT git+https://github.com/framer/motion.git 10.12.18 10.12.18 ^10.12.11 Framer 14 | kessler stuff react perpetual material MIT git+https://github.com/facebook/react.git 18.2.0 18.2.0 ^18.2.0 n/a 15 | kessler stuff react-dom perpetual material MIT git+https://github.com/facebook/react.git 18.2.0 18.2.0 ^18.2.0 n/a 16 | kessler stuff react-icons perpetual material MIT git+ssh://git@github.com/react-icons/react-icons.git 4.10.1 4.10.1 ^4.8.0 Goran Gajic 17 | kessler stuff @types/react perpetual material MIT https://github.com/DefinitelyTyped/DefinitelyTyped.git 18.2.14 18.2.14 ^18.0.28 n/a 18 | kessler stuff @types/react-dom perpetual material MIT https://github.com/DefinitelyTyped/DefinitelyTyped.git 18.2.6 18.2.6 ^18.0.11 n/a 19 | kessler stuff @typescript-eslint/eslint-plugin perpetual material MIT git+https://github.com/typescript-eslint/typescript-eslint.git 5.62.0 5.62.0 ^5.57.1 n/a 20 | kessler stuff @typescript-eslint/parser perpetual material BSD-2-Clause git+https://github.com/typescript-eslint/typescript-eslint.git 5.62.0 5.62.0 ^5.57.1 n/a 21 | kessler stuff @vitejs/plugin-react perpetual material MIT git+https://github.com/vitejs/vite-plugin-react.git 4.0.3 4.0.3 ^4.0.0 Evan You 22 | kessler stuff eslint perpetual material MIT git+https://github.com/eslint/eslint.git 8.44.0 8.44.0 ^8.38.0 Nicholas C. Zakas 23 | kessler stuff eslint-plugin-react-hooks perpetual material MIT git+https://github.com/facebook/react.git 4.6.0 4.6.0 ^4.6.0 n/a 24 | kessler stuff eslint-plugin-react-refresh perpetual material MIT git+https://github.com/ArnaudBarre/eslint-plugin-react-refresh.git 0.3.5 0.3.5 ^0.3.4 Arnaud Barré (https://github.com/ArnaudBarre) 25 | kessler stuff typescript perpetual material Apache-2.0 git+https://github.com/Microsoft/TypeScript.git 5.1.6 5.1.6 ^5.0.2 Microsoft Corp. 26 | kessler stuff vite perpetual material MIT git+https://github.com/vitejs/vite.git 4.4.2 4.4.2 ^4.3.2 Evan You 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JP RAG SAMPLE 2 | 3 | > [!IMPORTANT] 4 | > v0.4.0 より Amplify v1 から CDK に移行しました。以前のバージョンをお使いの方は[移行ガイド](docs/CDKMigration.md)をご覧ください。 5 | 6 | **JP RAG SAMPLE は、企業の知識ベースを活用して検索クエリに対する的確な回答を生成するためのオープンソースプロジェクトです。** 7 | 8 | 従来の検索エンジンでは、キーワードの一致度合いでヒット結果を返すため、ユーザーの本当の意図に沿った回答を得ることが難しい場合がありました。本ソリューションでは、企業の最新のナレッジベースから関連情報を検索し、大規模言語モデル(LLM)に与えることで、ユーザーの検索意図に沿った自然な回答を生成します。 9 | 10 | ![検索結果とAI生成回答の例](docs/png/rag-screenshot.png) 11 | 12 | ## 主な利用シーン 13 | 14 | - **カスタマーサポート**: 製品マニュアルや既知の問題集から最新の情報を参照し、ユーザーの問い合わせに的確に回答 15 | - **社内FAQ**: 社内ナレッジベースを活用し、社員の問い合わせに簡潔に回答 16 | - **マーケティング&セールス**: 最新の製品カタログや営業資料を参照し、顧客の質問に最適な情報を提示 17 | 18 | ## 導入メリット 19 | 20 | - 📘 **関連情報の参照**: 企業の知識ベースから関連情報を参照し、検索意図に沿った回答を生成 21 | - 🔍 **自然な検索体験**: 最新のLLMと組み合わせることで、キーワード検索を超えた自然な検索体験を実現 22 | - 📈 **検索精度の向上**: LLMが関連情報を総合的に判断することで、従来の検索よりも的確な回答を導出 23 | 24 | ## 主な機能 25 | 26 | ### 1. フルマネージドな Retriever (Amazon Kendra) 27 | 28 | 本ソリューションの検索エンジン部分には、AWS の AI サービス **Amazon Kendra** を利用しています。Kendra は完全マネージド型のサービスで、事前学習済みの AI モデルが組み込まれており、関連度の高いドキュメントを取り出すことができます。 29 | 30 | これまでは検索アプリケーションを運用する場合、データを取り込むコネクターの開発、全文データベースの運用、ベクトル生成用のアルゴリズム開発などが必要でした。一方 Amazon Kendra はフルマネージドサービスであるためそれらの開発・運用は不要です。Amazon Kendra には、Amazon Simple Storage Service (Amazon S3)、SharePoint、Confluence、ウェブサイトなどの一般的なデータソースへのコネクタがあらかじめ組み込まれており、HTML、Word、PowerPoint、PDF、Excel、テキストファイルなどの一般的なドキュメント形式もサポートしています。エンドユーザーの権限で許可されているドキュメントのみに基づいて応答をフィルタリングするために、アクセス制御リスト (ACL) にも対応しており、エンタープライズ企業での導入実績もあります。 31 | 32 | ### 2. 高度な生成 AI (Anthropic Claude 3 Haiku) 33 | 34 | 生成 AI 部分には、**Anthropic Claude 3 Haiku** を利用しています。Claude 3 Haikuは、高速な応答性と自然な対話を実現するよう設計された、コンパクトな大規模言語モデルです。 35 | 36 | ### 3. その他の特徴 37 | 38 | - **フィルター検索**: ドキュメントの種類、作成日時などでフィルタリングが可能 39 | - **Incremental Learning (英語のみ)**: 検索結果の良し悪しフィードバックを次回以降に反映 40 | - **セキュリティ**: WAF、MFA、IP制限、メールドメイン制限、SAML連携などを実装 41 | - **多言語対応**: 日本語を含め多言語に対応 42 | 43 | ## アーキテクチャ概要 44 | 45 | 本ソリューションは以下のようなアーキテクチャで構成されています。 46 | 47 | ![アーキテクチャ概要図](/docs/png/arch.drawio.png) 48 | 49 | ## クイックスタート 50 | 51 | ソリューションのデプロイ方法は[開発者ガイド](./docs/DeveloperGuide.md)を参照してください。 52 | 53 | ## Search Flow / 検索の流れ 54 | 55 | 検索の流れは以下のとおりです。 56 | 57 | ![](docs/png/search-flow.png) 58 | 59 | 60 | ## コスト 61 | 62 | ご利用いただく際の、構成と料金試算例が以下になります。従量課金制となっており、実際の料金はご利用内容により変動いたします。 63 | 64 | | サービス | 項目 | 数量 | 単価 | 料金 (USD) | 65 | | :------------|----------|--------- | --------|------------:| 66 | | Amazon Kendra | Developer Edition | 730h | $1.125 / h | 810 | 67 | | | Connector でスキャンしたドキュメント数 | 5,000 ドキュメント | 0.000001 USD/ドキュメント | 0.01 | 68 | | | Connector でスキャンした時間 | 30 時間 | 0.35 USD/時間 | 10.50 | 69 | | Amazon Bedrock | Claud 3 Haiku 入力トークン | 11,000,000 トークン | 0.00025 USD/1000 トークン | 2.75 | 70 | | | Claud 3 Haiku 出力トークン | 4,400,000 トークン | 0.00125 USD/1000 トークン | 5.5 | 71 | | AWS Lambda | 割り当てたメモリと実行時間 | 37,500 GB-秒 | 0.000016667 USD/GB-秒あたり | 0.63 | 72 | | | Lambda HTTP 応答ストリーム処理バイト | 1 GB | 0.008 USD/GB | 0.01 | 73 | | Amazon API Gateway | REST API リクエスト数 | 15,000 リクエスト | 4.25 USD/100 万リクエスト | 0.06 | 74 | |Amazon S3 | ストレージ容量 | 0.01 GB | 0.025 USD/GB | 0 | 75 | | | GET、SELECT リクエスト数 | 1,000 リクエスト | 0.00037 USD/1000 リクエスト | 0 | 76 | | Amazon CloudFront | データ転送 (OUT) | 1 GB | 0.114 USD/時間 | 0.11 | 77 | | |HTTPS リクエスト | 30,000 リクエスト | 0.012 USD/1万リクエスト | 0.04 | 78 | | Amazon Cognito | アクティブユーザー数 | 50 ユーザー | $0.0055 /ユーザー | 0.28 | 79 | | 合計 | | | | 829.89 | 80 | 81 | * 価格は執筆時点での内容になります。最新情報は [AWS 公式ウェブサイト](https://aws.amazon.com/)にてご確認ください。 82 | 83 | ## CONTRIBUTING 84 | 85 | 本プロジェクトへの質問や改善提案は、[GitHub Issues](https://github.com/aws-samples/jp-rag-sample/issues) よりお願いします。 86 | 87 | 詳細については [CONTRIBUTING](/CONTRIBUTING.md) もご確認ください。 88 | 89 | ## LICENSE 90 | 91 | Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. 92 | Licensed under the [MIT-0 License](https://github.com/aws/mit-0) 93 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # セキュリティに関する問題の連絡 2 | このプロジェクトで潜在的なセキュリティの問題を発見した場合は、 [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/)へご連絡下さい。 -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # SUPPORT 2 | 3 | opensource-codeofconduct@amazon.com を通じた、バグ・機能追加に関するリクエストを歓迎します。 4 | 5 | バグをご連絡頂くときは、できるだけ多くの情報を含めるようお願いいたします。 6 | 7 | - 再現可能なテストケースまたは一連の手順 8 | - 使用されているコードのバージョン 9 | - バグに関連して行った変更 10 | - OS, ミドルウェア等の環境とそのバージョン -------------------------------------------------------------------------------- /THIRD_PARTY_LICENSE: -------------------------------------------------------------------------------- 1 | 2 | ## Javascript/TypeScript App Third Party License List 3 | 4 | department related to name license period material / not material license type link remote version installed version defined version author 5 | ---------- ---------- ---- -------------- ----------------------- ------------ ---- -------------- ----------------- --------------- ------ 6 | kessler stuff @aws-amplify/ui-react perpetual material Apache-2.0 git+https://github.com/aws-amplify/amplify-ui.git 5.3.1 5.0.4 ^5.0.1 n/a 7 | kessler stuff @aws-sdk/client-kendra perpetual material Apache-2.0 git+https://github.com/aws/aws-sdk-js-v3.git 3.427.0 3.363.0 ^3.334.0 AWS SDK for JavaScript Team https://aws.amazon.com/javascript/ 8 | kessler stuff @chakra-ui/icons perpetual material MIT git+https://github.com/chakra-ui/chakra-ui.git 2.1.1 2.0.19 ^2.0.19 Segun Adebayo 9 | kessler stuff @chakra-ui/react perpetual material MIT git+https://github.com/chakra-ui/chakra-ui.git 2.8.1 2.7.1 ^2.6.1 Segun Adebayo 10 | kessler stuff @emotion/react perpetual material MIT git+https://github.com/emotion-js/emotion.git#main 11.11.1 11.11.1 ^11.11.0 Emotion Contributors 11 | kessler stuff @emotion/styled perpetual material MIT git+https://github.com/emotion-js/emotion.git#main 11.11.0 11.11.0 ^11.11.0 n/a 12 | kessler stuff @types/node perpetual material MIT https://github.com/DefinitelyTyped/DefinitelyTyped.git 20.8.4 20.4.1 ^20.1.7 n/a 13 | kessler stuff aws-amplify perpetual material Apache-2.0 git+https://github.com/aws-amplify/amplify-js.git 5.3.11 5.3.3 ^5.2.5 Amazon Web Services 14 | kessler stuff framer-motion perpetual material MIT git+https://github.com/framer/motion.git 10.16.4 10.12.18 ^10.12.11 Framer 15 | kessler stuff i18next perpetual material MIT git+https://github.com/i18next/i18next.git 23.5.1 23.5.1 ^23.5.1 Jan Mühlemann (https://github.com/jamuhl) 16 | kessler stuff qrcode perpetual material MIT git://github.com/soldair/node-qrcode.git 1.5.3 1.5.3 ^1.5.3 Ryan Day 17 | kessler stuff react perpetual material MIT git+https://github.com/facebook/react.git 18.2.0 18.2.0 ^18.2.0 n/a 18 | kessler stuff react-dom perpetual material MIT git+https://github.com/facebook/react.git 18.2.0 18.2.0 ^18.2.0 n/a 19 | kessler stuff react-i18next perpetual material MIT git+https://github.com/i18next/react-i18next.git 13.2.2 13.2.2 ^13.2.2 Jan Mühlemann (https://github.com/jamuhl) 20 | kessler stuff react-icons perpetual material MIT git+ssh://git@github.com/react-icons/react-icons.git 4.11.0 4.10.1 ^4.8.0 Goran Gajic 21 | kessler stuff recast perpetual material MIT git://github.com/benjamn/recast.git 0.21.0 0.21.0 0.21.0 Ben Newman 22 | 23 | 24 | 25 | ## Python App Third Party License List 26 | 27 | Name Version License 28 | aiohttp 3.8.6 Apache Software License 29 | aiosignal 1.3.1 Apache Software License 30 | async-timeout 4.0.3 Apache Software License 31 | boto3 1.28.62 Apache Software License 32 | botocore 1.31.62 Apache Software License 33 | frozenlist 1.4.0 Apache Software License 34 | huggingface-hub 0.17.3 Apache Software License 35 | multidict 6.0.4 Apache Software License 36 | requests 2.31.0 Apache Software License 37 | s3transfer 0.7.0 Apache Software License 38 | tenacity 8.2.3 Apache Software License 39 | tokenizers 0.14.1 Apache Software License 40 | yarl 1.9.2 Apache Software License 41 | packaging 23.2 Apache Software License; BSD License 42 | python-dateutil 2.8.2 Apache Software License; BSD License 43 | sniffio 1.3.0 Apache Software License; MIT License 44 | click 8.1.7 BSD License 45 | fsspec 2023.9.2 BSD License 46 | httpcore 0.18.0 BSD License 47 | httpx 0.25.0 BSD License 48 | idna 3.4 BSD License 49 | jsonpatch 1.33 BSD License 50 | jsonpointer 2.4 BSD License 51 | numpy 1.24.4 BSD License 52 | starlette 0.27.0 BSD License 53 | uvicorn 0.23.2 BSD License 54 | PyYAML 6.0.1 MIT License 55 | SQLAlchemy 2.0.21 MIT License 56 | annotated-types 0.6.0 MIT License 57 | anthropic 0.2.10 MIT License 58 | anyio 3.7.1 MIT License 59 | attrs 23.1.0 MIT License 60 | charset-normalizer 3.3.0 MIT License 61 | dataclasses-json 0.6.1 MIT License 62 | exceptiongroup 1.1.3 MIT License 63 | fastapi 0.103.2 MIT License 64 | greenlet 3.0.0 MIT License 65 | h11 0.14.0 MIT License 66 | jmespath 1.0.1 MIT License 67 | langchain 0.0.310 MIT License 68 | langsmith 0.0.43 MIT License 69 | marshmallow 3.20.1 MIT License 70 | mypy-extensions 1.0.0 MIT License 71 | pydantic 2.4.2 MIT License 72 | pydantic_core 2.10.1 MIT License 73 | six 1.16.0 MIT License 74 | typing-inspect 0.9.0 MIT License 75 | urllib3 1.26.17 MIT License 76 | tqdm 4.66.1 MIT License; Mozilla Public License 2.0 (MPL 2.0) 77 | certifi 2023.7.22 Mozilla Public License 2.0 (MPL 2.0) 78 | typing_extensions 4.8.0 Python Software Foundation License 79 | filelock 3.12.4 The Unlicense (Unlicense) 80 | -------------------------------------------------------------------------------- /docs/CDKMigration.md: -------------------------------------------------------------------------------- 1 | # 移行ガイド v0.3.0 → v0.4.0 2 | 3 | このソリューションは v0.4.0 より、より簡単にソリューションを拡張しお客様の RAG アプリケーションをカスタマイズできるように Amplify v1 から CDK ベースに移行しました。v0.4.0 からはいくつか新機能が加わっているため v0.4.0 に移行いただくことをお勧めします。 4 | 5 | ## v0.4.0 への移行手順 6 | 7 | Kendra は既存のものをインポートして利用可能です。Cognito は新規作成になるため既存ユーザーの移行が必要です。必要に応じて v0.3.0 と v0.4.0 を並行稼働させることも可能です。 8 | 9 | 1. v0.4.0 以降のバージョンを取得 (`main` を `git pull`) 10 | 2. 既存の Kendra Index を使用する場合は[設定にて紐づけます](/docs/DeveloperGuide.md#既存の-kendra-index-を利用する) 11 | 3. [デプロイ手順](/docs/DeveloperGuide.md)に従ってデプロイします。 12 | 13 | ## v0.3.0 環境(旧バージョン)の削除 14 | 15 | v0.4.0 デプロイ後、既存の v0.3.0 環境を削除したい場合は、Amplify の CloudFormation をコンソールから削除することが可能です。 16 | 17 | ## v0.3.0 を引き続き使う方へ 18 | 19 | v0.3.0 は引き続き使用いただけます。仮に手元の環境が壊れ、既存の v0.3.0 環境を手元で構築し直す場合は以下の手順で既存設定を復元することが可能です。 20 | 21 | 1. `git checkout refs/tags/v0.3.0` 22 | 2. `amplify env import` -------------------------------------------------------------------------------- /docs/DeveloperGuide.md: -------------------------------------------------------------------------------- 1 | # 開発者ガイド (Developer Guide) 2 | 3 | ## デプロイ 4 | 5 | > [!IMPORTANT] 6 | > このリポジトリでは、デフォルトでバージニア北部リージョン (us-east-1) の Anthropic Claude 3 Haiku モデルを利用する設定になっています。[Model access 画面 (us-east-1)](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess)を開き、Anthropic Claude 3 Haiku にチェックして Save changes してください。 7 | 8 | JP-RAG-Sample のデプロイには [AWS Cloud Development Kit](https://aws.amazon.com/jp/cdk/)(以降 CDK)を利用します。CDK がインストールされていない方は事前に [CDK をインストール](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) してください。 9 | 10 | まず、以下のコマンドを実行してください。全てのコマンドはリポジトリのルートで実行してください。 11 | 12 | ```bash 13 | npm ci 14 | ``` 15 | 16 | CDK を利用したことがない場合、初回のみ [Bootstrap](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping.html) 作業が必要です。すでに Bootstrap された環境では以下のコマンドは不要です。 17 | 18 | ```bash 19 | npx -w packages/cdk cdk bootstrap 20 | ``` 21 | 22 | 続いて、以下のコマンドで AWS リソースをデプロイします。デプロイが完了するまで、お待ちください(20 分程度かかる場合があります)。 23 | 24 | ```bash 25 | npm run cdk:deploy 26 | ``` 27 | 28 | ## デプロイオプション 29 | 30 | [packages/cdk/cdk.json](/packages/cdk/cdk.json) で設定を管理しており、以下のような設定が可能です。 31 | 32 | #### 既存の Kendra Index を利用する 33 | 34 | デフォルトでは新規の Kendra Index が作成されますが、既存の Kendra Index をインポートして使用することが可能です。 35 | 36 | context の `kendraIndexArn` に Index の ARN を指定します。もし、既存の Kendra Index で S3 データソースを利用している場合は、`kendraDataSourceBucketName` にバケット名を指定します。 37 | 38 | 39 | ``` 40 | "kendraIndexArn": "arn:aws:kendra:::index/", 41 | "kendraDataSourceBucketName": "", 42 | ``` 43 | 44 | #### アクセス制限の設定 45 | 46 | デフォルトでは公開されている URL より新規のアカウントが発行可能ですが、新規アカウント発行を無効化することが可能です。管理者のみがアカウント発行するケースなどで利用できます。 47 | 48 | ``` 49 | "selfSignUpEnabled": false, 50 | ``` 51 | 52 | また、サインアップできる E メールのドメインを制限することも可能です。自社の社員のみサインアップ可能にしたい場合などに利用できます。 53 | 54 | ``` 55 | "allowedSignUpEmailDomains": ["xxx.co.jp"], 56 | ``` 57 | 58 | また、Google Workspace や Microsoft Entra ID (旧 Azure Active Directory) などの IdP が提供する SAML 認証機能と連携ができます。次に詳細な連携手順があります。こちらもご活用ください。 59 | 60 | - [Google Workspace と SAML 連携](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/898ea5edb3bb6327a897a752747dbef3124010dc/docs/SAML_WITH_GOOGLE_WORKSPACE.md) 61 | - [Microsoft Entra ID と SAML 連携](https://github.com/aws-samples/generative-ai-use-cases-jp/blob/898ea5edb3bb6327a897a752747dbef3124010dc/docs/SAML_WITH_ENTRA_ID.md) 62 | 63 | [packages/cdk/cdk.json](/packages/cdk/cdk.json) にて以下を編集してください。 64 | 65 | - samlAuthEnabled : `true` にすることで、SAML 専用の認証画面に切り替わります。Cognito user pools を利用した従来の認証機能は利用できなくなります。 66 | - samlCognitoDomainName : Cognito の App integration で設定する Cognito Domain 名を指定します。 67 | - samlCognitoFederatedIdentityProviderName : Cognito の Sign-in experience で設定する Identity Provider の名前を指定します。 68 | 69 | ``` 70 | "samlAuthEnabled": true, 71 | "samlCognitoDomainName": "your-preferred-name.auth.ap-northeast-1.amazoncognito.com", 72 | "samlCognitoFederatedIdentityProviderName": "EntraID", 73 | ``` 74 | 75 | Web アプリへのアクセスを IP で制限したい場合、AWS WAF による IP 制限を有効化することができます。[packages/cdk/cdk.json](/packages/cdk/cdk.json) の `allowedIpV4AddressRanges` では許可する IPv4 の CIDR を配列で指定することができ、`allowedIpV6AddressRanges` では許可する IPv6 の CIDR を配列で指定することができます。 76 | 77 | ``` 78 | "allowedIpV4AddressRanges": ["192.0.2.44/32"], 79 | "allowedIpV6AddressRanges": ["0:0:0:0:0:ffff:c000:22c/128"], 80 | ``` 81 | 82 | Web アプリへのアクセスをアクセス元の国で制限したい場合、AWS WAF による地理的制限を有効化することができます。[packages/cdk/cdk.json](/packages/cdk/cdk.json) の `allowedCountryCodes` で許可する国を Country Code の配列で指定することができます。 83 | 指定する国の Country Code は[ISO 3166-2 from wikipedia](https://en.wikipedia.org/wiki/ISO_3166-2)をご参照ください。 84 | 85 | ``` 86 | "allowedCountryCodes": ["JP"], 87 | ``` 88 | 89 | #### モデルの設定を変更する 90 | 91 | デフォルトでは `us-east-1` の `anthropic.claude-3-haiku-20240307-v1:0` を使用していますが、他のリージョン・モデルに切り替えることも可能です。 92 | 93 | ``` 94 | "modelRegion": "us-east-1", 95 | "modelIds": [ 96 | "anthropic.claude-3-haiku-20240307-v1:0" 97 | ], 98 | ``` 99 | 100 | #### 独自ドメイン 101 | 102 | Web サイトの URL としてカスタムドメインを使用することができます。同一 AWS アカウントの Route53 にパブリックホストゾーンが作成済みであることが必要です。パブリックホストゾーンについてはこちらをご参照ください: [パブリックホストゾーンの使用 - Amazon Route 53](https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/AboutHZWorkingWith.html) 103 | 104 | 同一 AWS アカウントにパブリックホストゾーンを持っていない場合は、AWS ACM による SSL 証明書の検証時に手動で DNS レコードを追加する方法や、Eメール検証を行う方法もあります。これらの方法を利用する場合は、CDK のドキュメントを参照してカスタマイズしてください: [aws-cdk-lib.aws_certificatemanager module · AWS CDK](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_certificatemanager-readme.html) 105 | 106 | cdk.json には以下の値を設定します。 107 | 108 | - `hostName` ... Web サイトのホスト名です。A レコードは CDK によって作成されます。事前に作成する必要はありません 109 | - `domainName` ... 事前に作成したパブリックホストゾーンのドメイン名です 110 | - `hostedZoneId` ... 事前に作成したパブリックホストゾーンのIDです 111 | 112 | ``` 113 | "hostName": "genai", 114 | "domainName": "example.com", 115 | "hostedZoneId": "XXXXXXXXXXXXXXXXXXXX", 116 | ``` 117 | 118 | ## ローカル開発 119 | 120 | ### バックエンド 121 | 122 | `packages/cdk` ディレクトリで `cdk watch` を実行すると Lambda への変更が高速で反映されます。詳細については[ドキュメント](https://cdkworkshop.com/ja/20-typescript/30-hello-cdk/300-cdk-watch.html) をご確認ください。 123 | 124 | ### フロントエンド 125 | 126 | 以下のコマンドで CloudFormation のアウトプットから必要な情報を取得しローカル環境を立ち上げます。Windows ユーザーの方も Git Bash で利用できます。 127 | 128 | ```bash 129 | npm run web:devw 130 | ``` 131 | 132 | ## プロジェクト構造についての解説 133 | 134 | ``` 135 | packages 136 | |-- web # フロントエンド 137 | |-- cdk # バックエンド 138 | |-- types # 共通の型定義 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/png/arch.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/arch.drawio.png -------------------------------------------------------------------------------- /docs/png/feature1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/feature1.png -------------------------------------------------------------------------------- /docs/png/feature2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/feature2.png -------------------------------------------------------------------------------- /docs/png/feature3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/feature3.png -------------------------------------------------------------------------------- /docs/png/flow-ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/flow-ai.png -------------------------------------------------------------------------------- /docs/png/flow-kendra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/flow-kendra.png -------------------------------------------------------------------------------- /docs/png/flow-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/flow-rag.png -------------------------------------------------------------------------------- /docs/png/kendra-frow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/kendra-frow.png -------------------------------------------------------------------------------- /docs/png/layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/layout.png -------------------------------------------------------------------------------- /docs/png/rag-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/rag-screenshot.png -------------------------------------------------------------------------------- /docs/png/search-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/docs/png/search-flow.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jp-rag-sample", 3 | "private": true, 4 | "version": "0.4.0", 5 | "scripts": { 6 | "lint": "run-s root:lint web:lint cdk:lint", 7 | "root:lint": "npx prettier --write .", 8 | "web:devw": "source ./setup-env.sh && VITE_APP_VERSION=${npm_package_version} npm -w packages/web run dev", 9 | "web:dev": "VITE_APP_VERSION=${npm_package_version} npm -w packages/web run dev", 10 | "web:build": "VITE_APP_VERSION=${npm_package_version} npm -w packages/web run build", 11 | "web:lint": "npm -w packages/web run lint", 12 | "cdk:deploy": "npm -w packages/cdk run cdk deploy -- --all", 13 | "cdk:destroy": "npm -w packages/cdk run cdk destroy", 14 | "cdk:lint": "npm -w packages/cdk run lint" 15 | }, 16 | "devDependencies": { 17 | "npm-run-all": "^4.1.5", 18 | "prettier": "^3.2.5", 19 | "prettier-plugin-tailwindcss": "^0.5.13" 20 | }, 21 | "workspaces": [ 22 | "packages/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/cdk/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | ], 8 | ignorePatterns: ['cdk.out'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | project: './tsconfig.json', 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | '@typescript-eslint/no-namespace': 'off', 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /packages/cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /packages/cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `npx cdk deploy` deploy this stack to your default AWS account/region 13 | * `npx cdk diff` compare deployed stack with current state 14 | * `npx cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /packages/cdk/bin/jp-rag-sample.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { JpRagSampleStack } from '../lib/jp-rag-sample-stack'; 5 | import { CloudFrontWafStack } from '../lib/cloud-front-waf-stack'; 6 | 7 | const app = new cdk.App(); 8 | 9 | const allowedIpV4AddressRanges: string[] | null = app.node.tryGetContext( 10 | 'allowedIpV4AddressRanges' 11 | )!; 12 | const allowedIpV6AddressRanges: string[] | null = app.node.tryGetContext( 13 | 'allowedIpV6AddressRanges' 14 | )!; 15 | const allowedCountryCodes: string[] | null = app.node.tryGetContext( 16 | 'allowedCountryCodes' 17 | )!; 18 | 19 | // Props for custom domain name 20 | const hostName = app.node.tryGetContext('hostName'); 21 | if ( 22 | typeof hostName != 'undefined' && 23 | typeof hostName != 'string' && 24 | hostName != null 25 | ) { 26 | throw new Error('hostName must be a string'); 27 | } 28 | const domainName = app.node.tryGetContext('domainName'); 29 | if ( 30 | typeof domainName != 'undefined' && 31 | typeof domainName != 'string' && 32 | domainName != null 33 | ) { 34 | throw new Error('domainName must be a string'); 35 | } 36 | const hostedZoneId = app.node.tryGetContext('hostedZoneId'); 37 | if ( 38 | typeof hostedZoneId != 'undefined' && 39 | typeof hostedZoneId != 'string' && 40 | hostedZoneId != null 41 | ) { 42 | throw new Error('hostedZoneId must be a string'); 43 | } 44 | 45 | // check hostName, domainName hostedZoneId are all set or none of them 46 | if ( 47 | !( 48 | (hostName && domainName && hostedZoneId) || 49 | (!hostName && !domainName && !hostedZoneId) 50 | ) 51 | ) { 52 | throw new Error( 53 | 'hostName, domainName and hostedZoneId must be set or none of them' 54 | ); 55 | } 56 | 57 | let cloudFrontWafStack: CloudFrontWafStack | undefined; 58 | 59 | // IP アドレス範囲(v4もしくはv6のいずれか)か地理的制限が定義されている場合のみ、CloudFrontWafStack をデプロイする 60 | if ( 61 | allowedIpV4AddressRanges || 62 | allowedIpV6AddressRanges || 63 | allowedCountryCodes || 64 | hostName 65 | ) { 66 | // WAF v2 は us-east-1 でのみデプロイ可能なため、Stack を分けている 67 | cloudFrontWafStack = new CloudFrontWafStack(app, 'CloudFrontWafStack', { 68 | env: { 69 | account: process.env.CDK_DEFAULT_ACCOUNT, 70 | region: 'us-east-1', 71 | }, 72 | allowedIpV4AddressRanges, 73 | allowedIpV6AddressRanges, 74 | allowedCountryCodes, 75 | hostName, 76 | domainName, 77 | hostedZoneId, 78 | crossRegionReferences: true, 79 | }); 80 | } 81 | 82 | const anonymousUsageTracking: boolean = !!app.node.tryGetContext( 83 | 'anonymousUsageTracking' 84 | ); 85 | 86 | new JpRagSampleStack(app, 'JpRagSampleStack', { 87 | env: { 88 | account: process.env.CDK_DEFAULT_ACCOUNT, 89 | region: process.env.CDK_DEFAULT_REGION, 90 | }, 91 | webAclId: cloudFrontWafStack?.webAclArn, 92 | crossRegionReferences: true, 93 | allowedIpV4AddressRanges, 94 | allowedIpV6AddressRanges, 95 | allowedCountryCodes, 96 | description: anonymousUsageTracking 97 | ? 'JP RAG Sample (uksb-6g16jk2y91)' 98 | : undefined, 99 | cert: cloudFrontWafStack?.cert, 100 | hostName, 101 | domainName, 102 | hostedZoneId, 103 | }); 104 | -------------------------------------------------------------------------------- /packages/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/jp-rag-sample.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "kendraIndexArn": null, 21 | "kendraDataSourceBucketName": null, 22 | "selfSignUpEnabled": true, 23 | "allowedSignUpEmailDomains": null, 24 | "samlAuthEnabled": false, 25 | "samlCognitoDomainName": "", 26 | "samlCognitoFederatedIdentityProviderName": "", 27 | "modelRegion": "us-east-1", 28 | "modelIds": [ 29 | "anthropic.claude-3-haiku-20240307-v1:0" 30 | ], 31 | "allowedIpV4AddressRanges": null, 32 | "allowedIpV6AddressRanges": null, 33 | "allowedCountryCodes": null, 34 | "hostName": null, 35 | "domainName": null, 36 | "hostedZoneId": null, 37 | "anonymousUsageTracking": true, 38 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 39 | "@aws-cdk/core:checkSecretUsage": true, 40 | "@aws-cdk/core:target-partitions": [ 41 | "aws", 42 | "aws-cn" 43 | ], 44 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 45 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 46 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 47 | "@aws-cdk/aws-iam:minimizePolicies": true, 48 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 49 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 50 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 51 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 52 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 53 | "@aws-cdk/core:enablePartitionLiterals": true, 54 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 55 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 56 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 57 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 58 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 59 | "@aws-cdk/aws-route53-patters:useCertificate": true, 60 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 61 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 62 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 63 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 64 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 65 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 66 | "@aws-cdk/aws-redshift:columnId": true, 67 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 68 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 69 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 70 | "@aws-cdk/aws-kms:aliasNameRef": true, 71 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 72 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 73 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 74 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 75 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 76 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 77 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 78 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 79 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 80 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, 81 | "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, 82 | "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, 83 | "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, 84 | "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, 85 | "@aws-cdk/aws-eks:nodegroupNameAttribute": true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/cdk/lambda/checkEmailDomain.ts: -------------------------------------------------------------------------------- 1 | import { PreSignUpTriggerEvent, Context, Callback } from 'aws-lambda'; 2 | 3 | const ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR = 4 | process.env.ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR; 5 | const ALLOWED_SIGN_UP_EMAIL_DOMAINS: string[] = JSON.parse( 6 | ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR! 7 | ); 8 | 9 | // メールアドレスのドメインを許可するかどうかを判定する 10 | const checkEmailDomain = (email: string): boolean => { 11 | // メールアドレスの中の @ の数が1つでない場合は、常に許可しない 12 | if (email.split('@').length !== 2) { 13 | return false; 14 | } 15 | 16 | // メールアドレスのドメイン部分が、許可ドメインの"いずれか"と一致すれば許可する 17 | // それ以外の場合は、許可しない 18 | // (ALLOWED_SIGN_UP_EMAIL_DOMAINSが空の場合は、常に許可しない) 19 | const domain = email.split('@')[1]; 20 | return ALLOWED_SIGN_UP_EMAIL_DOMAINS.includes(domain); 21 | }; 22 | 23 | /** 24 | * Cognito Pre Sign-up Lambda Trigger. 25 | * 26 | * @param event - The event from Cognito. 27 | * @param context - The Lambda execution context. 28 | * @param callback - The callback function to return data or error. 29 | */ 30 | exports.handler = async ( 31 | event: PreSignUpTriggerEvent, 32 | context: Context, 33 | callback: Callback 34 | ) => { 35 | try { 36 | console.log('Received event:', JSON.stringify(event, null, 2)); 37 | 38 | const isAllowed = checkEmailDomain(event.request.userAttributes.email); 39 | if (isAllowed) { 40 | // 成功した場合、イベントオブジェクトをそのまま返す 41 | callback(null, event); 42 | } else { 43 | // 失敗した場合、エラーメッセージを返す 44 | callback(new Error('Invalid email domain')); 45 | } 46 | } catch (error) { 47 | console.log('Error ocurred:', error); 48 | // エラーがError型であるか確認し、適切なエラーメッセージを返す 49 | if (error instanceof Error) { 50 | callback(error); 51 | } else { 52 | // エラーがError型ではない場合、一般的なエラーメッセージを返す 53 | callback(new Error('An unknown error occurred.')); 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /packages/cdk/lambda/kendra.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { 3 | KendraClient, 4 | QueryCommand, 5 | SubmitFeedbackCommand, 6 | DescribeIndexCommand, 7 | ListDataSourcesCommand, 8 | QueryCommandOutput, 9 | } from '@aws-sdk/client-kendra'; 10 | import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; 11 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 12 | 13 | const kendra_client = new KendraClient({}); 14 | const s3_client = new S3Client({}); 15 | 16 | export const handler = async ( 17 | event: APIGatewayProxyEvent 18 | ): Promise => { 19 | const body = JSON.parse(event.body || '{}'); 20 | switch (event.httpMethod) { 21 | case 'POST': 22 | switch (event.path) { 23 | case '/kendra/query': 24 | return await kendraQuery(body); 25 | case '/kendra/send': 26 | return await kendraSend(body); 27 | case '/kendra/describeIndex': 28 | return await kendraDescribe(body); 29 | case '/kendra/listDataSources': 30 | return await kendraListDataSources(body); 31 | default: 32 | return { 33 | statusCode: 404, 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | 'Access-Control-Allow-Origin': '*', 37 | }, 38 | body: JSON.stringify({ message: 'Not found' }), 39 | }; 40 | } 41 | default: 42 | return { 43 | statusCode: 405, 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | 'Access-Control-Allow-Origin': '*', 47 | }, 48 | body: JSON.stringify({ message: 'Method not allowed' }), 49 | }; 50 | } 51 | }; 52 | 53 | async function kendraQuery(body: QueryCommand): Promise { 54 | const requestBody = body.input; 55 | const response = await kendra_client.send(new QueryCommand(requestBody)); 56 | const convertedResponse = await convertS3Url(response); 57 | return { 58 | statusCode: 200, 59 | headers: { 60 | 'Content-Type': 'application/json', 61 | 'Access-Control-Allow-Origin': '*', 62 | }, 63 | body: JSON.stringify(convertedResponse), 64 | }; 65 | } 66 | 67 | async function kendraSend( 68 | body: SubmitFeedbackCommand 69 | ): Promise { 70 | const requestBody = body.input; 71 | const response = await kendra_client.send( 72 | new SubmitFeedbackCommand(requestBody) 73 | ); 74 | return { 75 | statusCode: 200, 76 | headers: { 77 | 'Content-Type': 'application/json', 78 | 'Access-Control-Allow-Origin': '*', 79 | }, 80 | body: JSON.stringify(response), 81 | }; 82 | } 83 | 84 | async function kendraDescribe( 85 | body: DescribeIndexCommand 86 | ): Promise { 87 | const requestBody = body.input; 88 | const response = await kendra_client.send( 89 | new DescribeIndexCommand(requestBody) 90 | ); 91 | return { 92 | statusCode: 200, 93 | headers: { 94 | 'Content-Type': 'application/json', 95 | 'Access-Control-Allow-Origin': '*', 96 | }, 97 | body: JSON.stringify(response), 98 | }; 99 | } 100 | 101 | async function kendraListDataSources( 102 | body: ListDataSourcesCommand 103 | ): Promise { 104 | const requestBody = body.input; 105 | const response = await kendra_client.send( 106 | new ListDataSourcesCommand(requestBody) 107 | ); 108 | return { 109 | statusCode: 200, 110 | headers: { 111 | 'Content-Type': 'application/json', 112 | 'Access-Control-Allow-Origin': '*', 113 | }, 114 | body: JSON.stringify(response), 115 | }; 116 | } 117 | 118 | async function convertS3Url( 119 | data: QueryCommandOutput 120 | ): Promise { 121 | if (data && data.ResultItems) { 122 | for (const result of data.ResultItems) { 123 | if (result && result.DocumentId) { 124 | try { 125 | if (result.DocumentId.startsWith('s3')) { 126 | const [, , bucket, ...key] = result.DocumentId.split('/'); 127 | const command = new GetObjectCommand({ 128 | Bucket: bucket, 129 | Key: key.join('/'), 130 | }); 131 | const signedUrl = await getSignedUrl(s3_client, command, { 132 | expiresIn: 3600, 133 | }); 134 | result.DocumentURI = signedUrl; 135 | } 136 | } catch (error) { 137 | console.error('Error converting S3 URL:', error); 138 | } 139 | } 140 | } 141 | } 142 | return data; 143 | } 144 | -------------------------------------------------------------------------------- /packages/cdk/lambda/kendraSync.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { CloudFormationCustomResourceEvent, Context } from 'aws-lambda'; 5 | import { 6 | KendraClient, 7 | StartDataSourceSyncJobCommand, 8 | } from '@aws-sdk/client-kendra'; 9 | import * as cfnresponse from 'cfn-response'; 10 | 11 | const logger = console; 12 | 13 | const INDEX_ID = process.env['INDEX_ID'] || ''; 14 | const DS_ID = process.env['DS_ID'] || ''; 15 | const AWS_REGION = process.env['AWS_REGION'] || ''; 16 | const KENDRA = new KendraClient({ region: AWS_REGION }); 17 | 18 | const startDataSourceSync = async (dsId: string, indexId: string) => { 19 | logger.info(`start_data_source_sync(dsId=${dsId}, indexId=${indexId})`); 20 | const command = new StartDataSourceSyncJobCommand({ 21 | Id: dsId, 22 | IndexId: indexId, 23 | }); 24 | const response = await KENDRA.send(command); 25 | logger.info(`response: ${JSON.stringify(response)}`); 26 | }; 27 | 28 | export const handler = async ( 29 | event: CloudFormationCustomResourceEvent, 30 | context: Context 31 | ) => { 32 | await startDataSourceSync(DS_ID, INDEX_ID); 33 | const status: cfnresponse.ResponseStatus = cfnresponse.SUCCESS; 34 | cfnresponse.send(event, context, status, {}); 35 | return status; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cdk/lambda/predictStream.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from 'aws-lambda'; 2 | import { PredictRequest } from 'jp-rag-sample'; 3 | import api from './utils/api'; 4 | import { defaultModel } from './utils/models'; 5 | 6 | declare global { 7 | namespace awslambda { 8 | function streamifyResponse( 9 | f: ( 10 | event: PredictRequest, 11 | responseStream: NodeJS.WritableStream, 12 | context: Context 13 | ) => Promise 14 | ): Handler; 15 | } 16 | } 17 | 18 | export const handler = awslambda.streamifyResponse( 19 | async (event, responseStream, context) => { 20 | context.callbackWaitsForEmptyEventLoop = false; 21 | const model = event.model || defaultModel; 22 | for await (const token of api.invokeStream?.( 23 | model, 24 | event.messages, 25 | event.id 26 | ) ?? []) { 27 | responseStream.write(token); 28 | } 29 | responseStream.end(); 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /packages/cdk/lambda/utils/api.ts: -------------------------------------------------------------------------------- 1 | import bedrockApi from './bedrockApi'; 2 | 3 | const api = bedrockApi; 4 | 5 | export default api; 6 | -------------------------------------------------------------------------------- /packages/cdk/lambda/utils/bedrockApi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BedrockRuntimeClient, 3 | InvokeModelCommand, 4 | InvokeModelWithResponseStreamCommand, 5 | ServiceQuotaExceededException, 6 | ThrottlingException, 7 | } from '@aws-sdk/client-bedrock-runtime'; 8 | import { 9 | ApiInterface, 10 | BedrockResponse, 11 | UnrecordedMessage, 12 | } from 'jp-rag-sample'; 13 | import { BEDROCK_MODELS } from './models'; 14 | 15 | const client = new BedrockRuntimeClient({ 16 | region: process.env.MODEL_REGION, 17 | }); 18 | 19 | const createBodyText = ( 20 | model: string, 21 | messages: UnrecordedMessage[], 22 | id: string 23 | ): string => { 24 | const modelConfig = BEDROCK_MODELS[model]; 25 | return modelConfig.createBodyText(messages, id); 26 | }; 27 | 28 | const extractOutputText = (model: string, body: BedrockResponse): string => { 29 | const modelConfig = BEDROCK_MODELS[model]; 30 | return modelConfig.extractOutputText(body); 31 | }; 32 | 33 | const bedrockApi: ApiInterface = { 34 | invoke: async (model, messages, id) => { 35 | const command = new InvokeModelCommand({ 36 | modelId: model.modelId, 37 | body: createBodyText(model.modelId, messages, id), 38 | contentType: 'application/json', 39 | }); 40 | const data = await client.send(command); 41 | const body = JSON.parse(data.body.transformToString()); 42 | return extractOutputText(model.modelId, body); 43 | }, 44 | invokeStream: async function* (model, messages, id) { 45 | try { 46 | const command = new InvokeModelWithResponseStreamCommand({ 47 | modelId: model.modelId, 48 | body: createBodyText(model.modelId, messages, id), 49 | contentType: 'application/json', 50 | }); 51 | const res = await client.send(command); 52 | 53 | if (!res.body) { 54 | return; 55 | } 56 | 57 | for await (const streamChunk of res.body) { 58 | if (!streamChunk.chunk?.bytes) { 59 | break; 60 | } 61 | const bytes = new TextDecoder('utf-8').decode(streamChunk.chunk?.bytes); 62 | const body = JSON.parse(bytes); 63 | const outputText = extractOutputText(model.modelId, body); 64 | if (outputText) { 65 | yield outputText; 66 | } 67 | if (body.stop_reason) { 68 | break; 69 | } 70 | } 71 | } catch (e) { 72 | if ( 73 | e instanceof ThrottlingException || 74 | e instanceof ServiceQuotaExceededException 75 | ) { 76 | yield 'ただいまアクセスが集中しているため時間をおいて試してみてください。'; 77 | } else { 78 | yield 'エラーが発生しました。時間をおいて試してみてください。'; 79 | } 80 | } 81 | }, 82 | }; 83 | 84 | export default bedrockApi; 85 | -------------------------------------------------------------------------------- /packages/cdk/lambda/utils/models.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BedrockResponse, 3 | ClaudeMessageParams, 4 | Model, 5 | UnrecordedMessage, 6 | } from 'jp-rag-sample'; 7 | 8 | // Default Models 9 | const modelId: string = JSON.parse(process.env.MODEL_IDS || '[]') 10 | .map((name: string) => name.trim()) 11 | .filter((name: string) => name)[0]!; 12 | 13 | export const defaultModel: Model = { 14 | type: 'bedrock', 15 | modelId: modelId, 16 | }; 17 | 18 | // Model Params 19 | 20 | const CLAUDE_MESSAGE_DEFAULT_PARAMS: ClaudeMessageParams = { 21 | max_tokens: 500, 22 | temperature: 0, 23 | top_k: 1, 24 | }; 25 | export type ClaudeMessageParamsUsecases = Record; 26 | const CLAUDE_MESSAGE_USECASE_PARAMS: ClaudeMessageParamsUsecases = { 27 | '/rag': { 28 | temperature: 0.0, 29 | }, 30 | }; 31 | 32 | // Model Config 33 | 34 | const createBodyTextClaudeMessage = ( 35 | messages: UnrecordedMessage[], 36 | id: string 37 | ) => { 38 | const system = messages.find((message) => message.role === 'system'); 39 | messages = messages.filter((message) => message.role !== 'system'); 40 | const body: ClaudeMessageParams = { 41 | anthropic_version: 'bedrock-2023-05-31', 42 | system: system?.content, 43 | messages: messages.map((message) => { 44 | return { 45 | role: message.role, 46 | content: [{ type: 'text', text: message.content }], 47 | }; 48 | }), 49 | ...CLAUDE_MESSAGE_DEFAULT_PARAMS, 50 | ...CLAUDE_MESSAGE_USECASE_PARAMS[id], 51 | }; 52 | return JSON.stringify(body); 53 | }; 54 | 55 | const extractOutputTextClaudeMessage = (body: BedrockResponse): string => { 56 | if (body.type === 'message') { 57 | return body.content[0].text; 58 | } else if (body.type === 'content_block_delta') { 59 | return body.delta.text; 60 | } 61 | return ''; 62 | }; 63 | 64 | export const BEDROCK_MODELS: { 65 | [key: string]: { 66 | createBodyText: (messages: UnrecordedMessage[], id: string) => string; 67 | extractOutputText: (body: BedrockResponse) => string; 68 | }; 69 | } = { 70 | 'anthropic.claude-3-opus-20240229-v1:0': { 71 | createBodyText: createBodyTextClaudeMessage, 72 | extractOutputText: extractOutputTextClaudeMessage, 73 | }, 74 | 'anthropic.claude-3-sonnet-20240229-v1:0': { 75 | createBodyText: createBodyTextClaudeMessage, 76 | extractOutputText: extractOutputTextClaudeMessage, 77 | }, 78 | 'anthropic.claude-3-haiku-20240307-v1:0': { 79 | createBodyText: createBodyTextClaudeMessage, 80 | extractOutputText: extractOutputTextClaudeMessage, 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /packages/cdk/lib/cloud-front-waf-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { 3 | Certificate, 4 | CertificateValidation, 5 | ICertificate, 6 | } from 'aws-cdk-lib/aws-certificatemanager'; 7 | import { HostedZone } from 'aws-cdk-lib/aws-route53'; 8 | import { Construct } from 'constructs'; 9 | import { CommonWebAcl } from './construct/common-web-acl'; 10 | 11 | interface CloudFrontWafStackProps extends StackProps { 12 | allowedIpV4AddressRanges: string[] | null; 13 | allowedIpV6AddressRanges: string[] | null; 14 | allowedCountryCodes: string[] | null; 15 | hostName?: string; 16 | domainName?: string; 17 | hostedZoneId?: string; 18 | } 19 | 20 | export class CloudFrontWafStack extends Stack { 21 | public readonly webAclArn: string; 22 | public readonly webAcl: CommonWebAcl; 23 | public readonly cert: ICertificate; 24 | 25 | constructor(scope: Construct, id: string, props: CloudFrontWafStackProps) { 26 | super(scope, id, props); 27 | 28 | if ( 29 | props.allowedIpV4AddressRanges || 30 | props.allowedIpV6AddressRanges || 31 | props.allowedCountryCodes 32 | ) { 33 | const webAcl = new CommonWebAcl(this, `WebAcl${id}`, { 34 | scope: 'CLOUDFRONT', 35 | allowedIpV4AddressRanges: props.allowedIpV4AddressRanges, 36 | allowedIpV6AddressRanges: props.allowedIpV6AddressRanges, 37 | allowedCountryCodes: props.allowedCountryCodes, 38 | }); 39 | 40 | new CfnOutput(this, 'WebAclId', { 41 | value: webAcl.webAclArn, 42 | }); 43 | this.webAclArn = webAcl.webAclArn; 44 | this.webAcl = webAcl; 45 | } 46 | 47 | if (props.hostName && props.domainName && props.hostedZoneId) { 48 | const hostedZone = HostedZone.fromHostedZoneAttributes( 49 | this, 50 | 'HostedZone', 51 | { 52 | hostedZoneId: props.hostedZoneId, 53 | zoneName: props.domainName, 54 | } 55 | ); 56 | const cert = new Certificate(this, 'Cert', { 57 | domainName: `${props.hostName}.${props.domainName}`, 58 | validation: CertificateValidation.fromDns(hostedZone), 59 | }); 60 | this.cert = cert; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/api.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { Cors, RestApi, ResponseType } from 'aws-cdk-lib/aws-apigateway'; 3 | import { UserPool } from 'aws-cdk-lib/aws-cognito'; 4 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 5 | import { Construct } from 'constructs'; 6 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 7 | import { IdentityPool } from '@aws-cdk/aws-cognito-identitypool-alpha'; 8 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 9 | 10 | export interface BackendApiProps { 11 | userPool: UserPool; 12 | idPool: IdentityPool; 13 | } 14 | 15 | export class Api extends Construct { 16 | readonly api: RestApi; 17 | readonly predictStreamFunction: NodejsFunction; 18 | readonly modelRegion: string; 19 | readonly modelIds: string[]; 20 | 21 | constructor(scope: Construct, id: string, props: BackendApiProps) { 22 | super(scope, id); 23 | 24 | const { idPool } = props; 25 | 26 | // region for bedrock 27 | const modelRegion = this.node.tryGetContext('modelRegion') || 'us-east-1'; 28 | 29 | // Model IDs 30 | const modelIds: string[] = this.node.tryGetContext('modelIds') || [ 31 | 'anthropic.claude-3-haiku-20240307-v1:0', 32 | ]; 33 | 34 | // Validate Model Names 35 | const supportedModelIds = [ 36 | 'anthropic.claude-3-opus-20240229-v1:0', 37 | 'anthropic.claude-3-sonnet-20240229-v1:0', 38 | 'anthropic.claude-3-haiku-20240307-v1:0', 39 | ]; 40 | 41 | for (const modelId of modelIds) { 42 | if (!supportedModelIds.includes(modelId)) { 43 | throw new Error(`Unsupported Model Name: ${modelId}`); 44 | } 45 | } 46 | 47 | const predictStreamFunction = new NodejsFunction(this, 'PredictStream', { 48 | runtime: Runtime.NODEJS_18_X, 49 | entry: './lambda/predictStream.ts', 50 | timeout: Duration.minutes(15), 51 | environment: { 52 | MODEL_REGION: modelRegion, 53 | MODEL_IDS: JSON.stringify(modelIds), 54 | }, 55 | bundling: { 56 | nodeModules: ['@aws-sdk/client-bedrock-runtime'], 57 | }, 58 | }); 59 | predictStreamFunction.grantInvoke(idPool.authenticatedRole); 60 | 61 | // Bedrock Policy 62 | const bedrockPolicy = new PolicyStatement({ 63 | effect: Effect.ALLOW, 64 | resources: ['*'], 65 | actions: ['bedrock:*', 'logs:*'], 66 | }); 67 | predictStreamFunction.role?.addToPrincipalPolicy(bedrockPolicy); 68 | 69 | const api = new RestApi(this, 'Api', { 70 | deployOptions: { 71 | stageName: 'api', 72 | }, 73 | defaultCorsPreflightOptions: { 74 | allowOrigins: Cors.ALL_ORIGINS, 75 | allowMethods: Cors.ALL_METHODS, 76 | }, 77 | cloudWatchRole: true, 78 | }); 79 | 80 | api.addGatewayResponse('Api4XX', { 81 | type: ResponseType.DEFAULT_4XX, 82 | responseHeaders: { 83 | 'Access-Control-Allow-Origin': "'*'", 84 | }, 85 | }); 86 | 87 | api.addGatewayResponse('Api5XX', { 88 | type: ResponseType.DEFAULT_5XX, 89 | responseHeaders: { 90 | 'Access-Control-Allow-Origin': "'*'", 91 | }, 92 | }); 93 | 94 | this.api = api; 95 | this.predictStreamFunction = predictStreamFunction; 96 | this.modelRegion = modelRegion; 97 | this.modelIds = modelIds; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/auth.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { 3 | UserPool, 4 | UserPoolClient, 5 | UserPoolOperation, 6 | Mfa, 7 | } from 'aws-cdk-lib/aws-cognito'; 8 | import { 9 | IdentityPool, 10 | UserPoolAuthenticationProvider, 11 | } from '@aws-cdk/aws-cognito-identitypool-alpha'; 12 | import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 13 | import { Construct } from 'constructs'; 14 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 15 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 16 | 17 | export interface AuthProps { 18 | selfSignUpEnabled: boolean; 19 | allowedIpV4AddressRanges: string[] | null; 20 | allowedIpV6AddressRanges: string[] | null; 21 | allowedSignUpEmailDomains: string[] | null | undefined; 22 | samlAuthEnabled: boolean; 23 | } 24 | 25 | export class Auth extends Construct { 26 | readonly userPool: UserPool; 27 | readonly client: UserPoolClient; 28 | readonly idPool: IdentityPool; 29 | 30 | constructor(scope: Construct, id: string, props: AuthProps) { 31 | super(scope, id); 32 | 33 | const userPool = new UserPool(this, 'UserPool', { 34 | // SAML 認証を有効化する場合、UserPool を利用したセルフサインアップは利用しない。セキュリティを意識して閉じる。 35 | selfSignUpEnabled: props.samlAuthEnabled 36 | ? false 37 | : props.selfSignUpEnabled, 38 | signInAliases: { 39 | username: false, 40 | email: true, 41 | }, 42 | passwordPolicy: { 43 | requireUppercase: true, 44 | requireSymbols: true, 45 | requireDigits: true, 46 | minLength: 8, 47 | }, 48 | mfa: Mfa.OPTIONAL, 49 | mfaSecondFactor: { 50 | sms: true, 51 | otp: true, 52 | }, 53 | }); 54 | 55 | const client = userPool.addClient('client', { 56 | idTokenValidity: Duration.days(1), 57 | }); 58 | 59 | const idPool = new IdentityPool(this, 'IdentityPool', { 60 | authenticationProviders: { 61 | userPools: [ 62 | new UserPoolAuthenticationProvider({ 63 | userPool, 64 | userPoolClient: client, 65 | }), 66 | ], 67 | }, 68 | }); 69 | 70 | if (props.allowedIpV4AddressRanges || props.allowedIpV6AddressRanges) { 71 | const ipRanges = [ 72 | ...(props.allowedIpV4AddressRanges 73 | ? props.allowedIpV4AddressRanges 74 | : []), 75 | ...(props.allowedIpV6AddressRanges 76 | ? props.allowedIpV6AddressRanges 77 | : []), 78 | ]; 79 | 80 | idPool.authenticatedRole.attachInlinePolicy( 81 | new Policy(this, 'SourceIpPolicy', { 82 | statements: [ 83 | new PolicyStatement({ 84 | effect: Effect.DENY, 85 | resources: ['*'], 86 | actions: ['*'], 87 | conditions: { 88 | NotIpAddress: { 89 | 'aws:SourceIp': ipRanges, 90 | }, 91 | }, 92 | }), 93 | ], 94 | }) 95 | ); 96 | } 97 | 98 | // Lambda 99 | if (props.allowedSignUpEmailDomains) { 100 | const checkEmailDomainFunction = new NodejsFunction( 101 | this, 102 | 'CheckEmailDomain', 103 | { 104 | runtime: Runtime.NODEJS_18_X, 105 | entry: './lambda/checkEmailDomain.ts', 106 | timeout: Duration.minutes(15), 107 | environment: { 108 | ALLOWED_SIGN_UP_EMAIL_DOMAINS_STR: JSON.stringify( 109 | props.allowedSignUpEmailDomains 110 | ), 111 | }, 112 | } 113 | ); 114 | 115 | userPool.addTrigger( 116 | UserPoolOperation.PRE_SIGN_UP, 117 | checkEmailDomainFunction 118 | ); 119 | } 120 | 121 | this.client = client; 122 | this.userPool = userPool; 123 | this.idPool = idPool; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/common-web-acl.ts: -------------------------------------------------------------------------------- 1 | import { CfnIPSet, CfnWebACL, CfnWebACLProps } from 'aws-cdk-lib/aws-wafv2'; 2 | import { Construct } from 'constructs'; 3 | 4 | export interface CommonWebAclProps { 5 | scope: 'REGIONAL' | 'CLOUDFRONT'; 6 | allowedIpV4AddressRanges: string[] | null; 7 | allowedIpV6AddressRanges: string[] | null; 8 | allowedCountryCodes: string[] | null; 9 | } 10 | 11 | export class CommonWebAcl extends Construct { 12 | public readonly webAclArn: string; 13 | 14 | constructor(scope: Construct, id: string, props: CommonWebAclProps) { 15 | super(scope, id); 16 | 17 | const rules: CfnWebACLProps['rules'] = []; 18 | 19 | const generateIpSetRule = ( 20 | priority: number, 21 | name: string, 22 | ipSetArn: string 23 | ) => ({ 24 | priority, 25 | name, 26 | action: { allow: {} }, 27 | visibilityConfig: { 28 | sampledRequestsEnabled: true, 29 | cloudWatchMetricsEnabled: true, 30 | metricName: name, 31 | }, 32 | statement: { 33 | ipSetReferenceStatement: { 34 | arn: ipSetArn, 35 | }, 36 | }, 37 | }); 38 | 39 | if (props.allowedIpV4AddressRanges) { 40 | const wafIPv4Set = new CfnIPSet(this, `IPv4Set${id}`, { 41 | ipAddressVersion: 'IPV4', 42 | scope: props.scope, 43 | addresses: props.allowedIpV4AddressRanges, 44 | }); 45 | rules.push(generateIpSetRule(1, `IpV4SetRule${id}`, wafIPv4Set.attrArn)); 46 | } 47 | 48 | if (props.allowedIpV6AddressRanges) { 49 | const wafIPv6Set = new CfnIPSet(this, `IPv6Set${id}`, { 50 | ipAddressVersion: 'IPV6', 51 | scope: props.scope, 52 | addresses: props.allowedIpV6AddressRanges, 53 | }); 54 | rules.push(generateIpSetRule(2, `IpV6SetRule${id}`, wafIPv6Set.attrArn)); 55 | } 56 | 57 | if (props.allowedCountryCodes) { 58 | rules.push({ 59 | priority: 3, 60 | name: `GeoMatchSetRule${id}`, 61 | action: { allow: {} }, 62 | visibilityConfig: { 63 | cloudWatchMetricsEnabled: true, 64 | metricName: 'FrontendWebAcl', 65 | sampledRequestsEnabled: true, 66 | }, 67 | statement: { 68 | geoMatchStatement: { 69 | countryCodes: props.allowedCountryCodes, 70 | }, 71 | }, 72 | }); 73 | } 74 | 75 | const webAcl = new CfnWebACL(this, `WebAcl${id}`, { 76 | defaultAction: { block: {} }, 77 | name: `WebAcl${id}`, 78 | scope: props.scope, 79 | visibilityConfig: { 80 | cloudWatchMetricsEnabled: true, 81 | sampledRequestsEnabled: true, 82 | metricName: `WebAcl${id}`, 83 | }, 84 | rules: rules, 85 | }); 86 | this.webAclArn = webAcl.attrArn; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './auth'; 3 | export * from './web'; 4 | export * from './rag'; 5 | export * from './common-web-acl'; 6 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/rag.ts: -------------------------------------------------------------------------------- 1 | import * as kendra from 'aws-cdk-lib/aws-kendra'; 2 | import * as iam from 'aws-cdk-lib/aws-iam'; 3 | import * as s3 from 'aws-cdk-lib/aws-s3'; 4 | import * as cr from 'aws-cdk-lib/custom-resources'; 5 | import { Construct } from 'constructs'; 6 | import { UserPool } from 'aws-cdk-lib/aws-cognito'; 7 | import { Token, Arn, RemovalPolicy } from 'aws-cdk-lib'; 8 | import { 9 | AuthorizationType, 10 | CognitoUserPoolsAuthorizer, 11 | LambdaIntegration, 12 | RestApi, 13 | } from 'aws-cdk-lib/aws-apigateway'; 14 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 15 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 16 | 17 | export interface RagProps { 18 | userPool: UserPool; 19 | api: RestApi; 20 | } 21 | 22 | /** 23 | * RAG を実行するためのリソースを作成する 24 | */ 25 | export class Rag extends Construct { 26 | readonly kendraIndexId: string; 27 | 28 | constructor(scope: Construct, id: string, props: RagProps) { 29 | super(scope, id); 30 | 31 | const kendraIndexArnInCdkContext = 32 | this.node.tryGetContext('kendraIndexArn'); 33 | 34 | const kendraDataSourceBucketName = this.node.tryGetContext( 35 | 'kendraDataSourceBucketName' 36 | ); 37 | 38 | let kendraIndexArn: string; 39 | let kendraIndexId: string; 40 | let dataSourceBucket: s3.IBucket | null = null; 41 | 42 | if (kendraIndexArnInCdkContext) { 43 | // 既存の Kendra Index を利用する場合 44 | kendraIndexArn = kendraIndexArnInCdkContext!; 45 | kendraIndexId = Arn.extractResourceName( 46 | kendraIndexArnInCdkContext, 47 | 'index' 48 | ); 49 | // 既存の S3 データソースを利用する場合は、バケット名からオブジェクトを生成 50 | if (kendraDataSourceBucketName) { 51 | dataSourceBucket = s3.Bucket.fromBucketName( 52 | this, 53 | 'DataSourceBucket', 54 | kendraDataSourceBucketName 55 | ); 56 | } 57 | 58 | this.kendraIndexId = kendraIndexId; 59 | } else { 60 | // 新規に Kendra Index を作成する場合 61 | const indexRole = new iam.Role(this, 'KendraIndexRole', { 62 | assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'), 63 | }); 64 | 65 | indexRole.addToPolicy( 66 | new iam.PolicyStatement({ 67 | effect: iam.Effect.ALLOW, 68 | resources: ['*'], 69 | actions: ['s3:GetObject'], 70 | }) 71 | ); 72 | 73 | indexRole.addManagedPolicy( 74 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') 75 | ); 76 | 77 | const index = new kendra.CfnIndex(this, 'KendraIndex', { 78 | name: 'jp-rag-sample', 79 | edition: 'DEVELOPER_EDITION', 80 | roleArn: indexRole.roleArn, 81 | 82 | // トークンベースのアクセス制御を実施 83 | // 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-index.html#cfn-kendra-index-usercontextpolicy 84 | userContextPolicy: 'USER_TOKEN', 85 | 86 | // 認可に利用する Cognito の情報を設定 87 | userTokenConfigurations: [ 88 | { 89 | jwtTokenTypeConfiguration: { 90 | keyLocation: 'URL', 91 | userNameAttributeField: 'cognito:username', 92 | groupAttributeField: 'cognito:groups', 93 | url: `${props.userPool.userPoolProviderUrl}/.well-known/jwks.json`, 94 | }, 95 | }, 96 | ], 97 | }); 98 | 99 | kendraIndexArn = Token.asString(index.getAtt('Arn')); 100 | kendraIndexId = index.ref; 101 | 102 | // .pdf や .txt などのドキュメントを格納する S3 Bucket 103 | dataSourceBucket = new s3.Bucket(this, 'DataSourceBucket', { 104 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 105 | encryption: s3.BucketEncryption.S3_MANAGED, 106 | autoDeleteObjects: true, 107 | removalPolicy: RemovalPolicy.DESTROY, 108 | objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, 109 | serverAccessLogsPrefix: 'AccessLogs/', 110 | enforceSSL: true, 111 | }); 112 | 113 | const s3DataSourceRole = new iam.Role(this, 'DataSourceRole', { 114 | assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'), 115 | }); 116 | 117 | s3DataSourceRole.addToPolicy( 118 | new iam.PolicyStatement({ 119 | effect: iam.Effect.ALLOW, 120 | resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}`], 121 | actions: ['s3:ListBucket'], 122 | }) 123 | ); 124 | 125 | s3DataSourceRole.addToPolicy( 126 | new iam.PolicyStatement({ 127 | effect: iam.Effect.ALLOW, 128 | resources: [`arn:aws:s3:::${dataSourceBucket.bucketName}/*`], 129 | actions: ['s3:GetObject'], 130 | }) 131 | ); 132 | 133 | s3DataSourceRole.addToPolicy( 134 | new iam.PolicyStatement({ 135 | effect: iam.Effect.ALLOW, 136 | resources: [Token.asString(index.getAtt('Arn'))], 137 | actions: ['kendra:BatchPutDocument', 'kendra:BatchDeleteDocument'], 138 | }) 139 | ); 140 | 141 | const dataSource = new kendra.CfnDataSource(this, 'S3DataSource', { 142 | indexId: index.ref, 143 | type: 'S3', 144 | name: 's3-data-source', 145 | roleArn: s3DataSourceRole.roleArn, 146 | languageCode: 'ja', 147 | dataSourceConfiguration: { 148 | s3Configuration: { 149 | bucketName: dataSourceBucket.bucketName, 150 | inclusionPrefixes: ['docs'], 151 | }, 152 | }, 153 | }); 154 | dataSource.addDependency(index); 155 | 156 | const dataSource2 = new kendra.CfnDataSource(this, 'WebDataSource', { 157 | indexId: index.ref, 158 | type: 'WEBCRAWLER', 159 | name: 'web-data-source', 160 | roleArn: s3DataSourceRole.roleArn, 161 | languageCode: 'ja', 162 | dataSourceConfiguration: { 163 | webCrawlerConfiguration: { 164 | urlInclusionPatterns: [ 165 | '.*https://docs.aws.amazon.com/ja_jp/lex/.*', 166 | '.*https://docs.aws.amazon.com/ja_jp/kendra/.*', 167 | '.*https://docs.aws.amazon.com/ja_jp/sagemaker/.*', 168 | ], 169 | urls: { 170 | siteMapsConfiguration: { 171 | siteMaps: [ 172 | 'https://docs.aws.amazon.com/ja_jp/lex/latest/dg/sitemap.xml', 173 | 'https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/sitemap.xml', 174 | 'https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/sitemap.xml', 175 | ], 176 | }, 177 | }, 178 | }, 179 | }, 180 | }); 181 | dataSource2.addDependency(index); 182 | 183 | // 初回構築時に Sync 184 | const dataSourceSyncLambda = new NodejsFunction( 185 | this, 186 | 'DataSourceSyncLambda', 187 | { 188 | runtime: Runtime.NODEJS_18_X, 189 | entry: './lambda/kendraSync.ts', 190 | bundling: { 191 | // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする 192 | externalModules: [], 193 | }, 194 | environment: { 195 | INDEX_ID: kendraIndexId, 196 | DS_ID: dataSource2.attrId, 197 | }, 198 | } 199 | ); 200 | dataSourceSyncLambda.addToRolePolicy( 201 | new iam.PolicyStatement({ 202 | effect: iam.Effect.ALLOW, 203 | resources: ['*'], 204 | actions: ['kendra:StartDataSourceSyncJob'], 205 | }) 206 | ); 207 | const dataSourceSync = new cr.AwsCustomResource(this, 'DataSourceSync', { 208 | onCreate: { 209 | service: 'Lambda', 210 | action: 'invoke', 211 | parameters: { 212 | FunctionName: dataSourceSyncLambda.functionName, 213 | Payload: JSON.stringify({}), 214 | }, 215 | physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), 216 | }, 217 | policy: cr.AwsCustomResourcePolicy.fromStatements([ 218 | new iam.PolicyStatement({ 219 | effect: iam.Effect.ALLOW, 220 | actions: ['lambda:InvokeFunction'], 221 | resources: [dataSourceSyncLambda.functionArn], 222 | }), 223 | ]), 224 | }); 225 | dataSourceSync.node.addDependency(index); 226 | 227 | this.kendraIndexId = index.ref; 228 | } 229 | 230 | // RAG 関連の API を追加する 231 | // Lambda 232 | const kendraFunction = new NodejsFunction(this, 'KendraFunction', { 233 | runtime: Runtime.NODEJS_18_X, 234 | entry: './lambda/kendra.ts', 235 | bundling: { 236 | // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする 237 | externalModules: [], 238 | }, 239 | environment: { 240 | INDEX_ID: kendraIndexId, 241 | }, 242 | }); 243 | kendraFunction.role?.addToPrincipalPolicy( 244 | new iam.PolicyStatement({ 245 | effect: iam.Effect.ALLOW, 246 | resources: [kendraIndexArn], 247 | actions: [ 248 | 'kendra:Describe*', 249 | 'kendra:List*', 250 | 'kendra:Query', 251 | 'kendra:GetQuerySuggestions', 252 | 'kendra:SubmitFeedback', 253 | 'kendra:ListDataSources', 254 | ], 255 | }) 256 | ); 257 | if (dataSourceBucket) { 258 | dataSourceBucket.grantRead(kendraFunction); 259 | } 260 | 261 | // API Gateway 262 | const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', { 263 | cognitoUserPools: [props.userPool], 264 | }); 265 | 266 | const commonAuthorizerProps = { 267 | authorizationType: AuthorizationType.COGNITO, 268 | authorizer, 269 | }; 270 | 271 | const kendraResource = props.api.root.addResource('kendra'); 272 | const queryResource = kendraResource.addResource('query'); 273 | queryResource.addMethod( 274 | 'POST', 275 | new LambdaIntegration(kendraFunction), 276 | commonAuthorizerProps 277 | ); 278 | const sendResource = kendraResource.addResource('send'); 279 | sendResource.addMethod( 280 | 'POST', 281 | new LambdaIntegration(kendraFunction), 282 | commonAuthorizerProps 283 | ); 284 | const describeIndexResource = kendraResource.addResource('describeIndex'); 285 | describeIndexResource.addMethod( 286 | 'POST', 287 | new LambdaIntegration(kendraFunction), 288 | commonAuthorizerProps 289 | ); 290 | const listDataSourcesResource = 291 | kendraResource.addResource('listDataSources'); 292 | listDataSourcesResource.addMethod( 293 | 'POST', 294 | new LambdaIntegration(kendraFunction), 295 | commonAuthorizerProps 296 | ); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/web.ts: -------------------------------------------------------------------------------- 1 | import { Stack, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { 4 | CloudFrontToS3, 5 | CloudFrontToS3Props, 6 | } from '@aws-solutions-constructs/aws-cloudfront-s3'; 7 | import { CfnDistribution, Distribution } from 'aws-cdk-lib/aws-cloudfront'; 8 | import { NodejsBuild } from 'deploy-time-build'; 9 | import * as s3 from 'aws-cdk-lib/aws-s3'; 10 | import { ARecord, HostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53'; 11 | import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets'; 12 | import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; 13 | 14 | export interface WebProps { 15 | apiEndpointUrl: string; 16 | kendraIndexId: string; 17 | userPoolId: string; 18 | userPoolClientId: string; 19 | idPoolId: string; 20 | predictStreamFunctionArn: string; 21 | selfSignUpEnabled: boolean; 22 | webAclId?: string; 23 | modelRegion: string; 24 | modelIds: string[]; 25 | samlAuthEnabled: boolean; 26 | samlCognitoDomainName: string; 27 | samlCognitoFederatedIdentityProviderName: string; 28 | cert?: ICertificate; 29 | hostName?: string; 30 | domainName?: string; 31 | hostedZoneId?: string; 32 | } 33 | 34 | export class Web extends Construct { 35 | public readonly distribution: Distribution; 36 | 37 | constructor(scope: Construct, id: string, props: WebProps) { 38 | super(scope, id); 39 | 40 | const commonBucketProps: s3.BucketProps = { 41 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 42 | encryption: s3.BucketEncryption.S3_MANAGED, 43 | autoDeleteObjects: true, 44 | removalPolicy: RemovalPolicy.DESTROY, 45 | objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, 46 | enforceSSL: true, 47 | }; 48 | 49 | const cloudFrontToS3Props: CloudFrontToS3Props = { 50 | insertHttpSecurityHeaders: false, 51 | loggingBucketProps: commonBucketProps, 52 | bucketProps: commonBucketProps, 53 | cloudFrontLoggingBucketProps: commonBucketProps, 54 | cloudFrontDistributionProps: { 55 | errorResponses: [ 56 | { 57 | httpStatus: 403, 58 | responseHttpStatus: 200, 59 | responsePagePath: '/index.html', 60 | }, 61 | { 62 | httpStatus: 404, 63 | responseHttpStatus: 200, 64 | responsePagePath: '/index.html', 65 | }, 66 | ], 67 | }, 68 | }; 69 | 70 | if ( 71 | props.cert && 72 | props.hostName && 73 | props.domainName && 74 | props.hostedZoneId 75 | ) { 76 | cloudFrontToS3Props.cloudFrontDistributionProps.certificate = props.cert; 77 | cloudFrontToS3Props.cloudFrontDistributionProps.domainNames = [ 78 | `${props.hostName}.${props.domainName}`, 79 | ]; 80 | } 81 | 82 | const { cloudFrontWebDistribution, s3BucketInterface } = new CloudFrontToS3( 83 | this, 84 | 'Web', 85 | cloudFrontToS3Props 86 | ); 87 | 88 | if ( 89 | props.cert && 90 | props.hostName && 91 | props.domainName && 92 | props.hostedZoneId 93 | ) { 94 | // DNS record for custom domain 95 | const hostedZone = HostedZone.fromHostedZoneAttributes( 96 | this, 97 | 'HostedZone', 98 | { 99 | hostedZoneId: props.hostedZoneId, 100 | zoneName: props.domainName, 101 | } 102 | ); 103 | new ARecord(this, 'ARecord', { 104 | zone: hostedZone, 105 | recordName: props.hostName, 106 | target: RecordTarget.fromAlias( 107 | new CloudFrontTarget(cloudFrontWebDistribution) 108 | ), 109 | }); 110 | } 111 | 112 | if (props.webAclId) { 113 | const existingCloudFrontWebDistribution = cloudFrontWebDistribution.node 114 | .defaultChild as CfnDistribution; 115 | existingCloudFrontWebDistribution.addPropertyOverride( 116 | 'DistributionConfig.WebACLId', 117 | props.webAclId 118 | ); 119 | } 120 | 121 | new NodejsBuild(this, 'BuildWeb', { 122 | assets: [ 123 | { 124 | path: '../../', 125 | exclude: [ 126 | '.git', 127 | '.github', 128 | '.gitignore', 129 | '.prettierignore', 130 | '.prettierrc.json', 131 | '*.md', 132 | 'LICENSE', 133 | 'docs', 134 | 'imgs', 135 | 'setup-env.sh', 136 | 'node_modules', 137 | 'prompt-templates', 138 | 'packages/cdk/**/*', 139 | '!packages/cdk/cdk.json', 140 | 'packages/web/dist', 141 | 'packages/web/dev-dist', 142 | 'packages/web/node_modules', 143 | 'browser-extension', 144 | ], 145 | }, 146 | ], 147 | destinationBucket: s3BucketInterface, 148 | distribution: cloudFrontWebDistribution, 149 | outputSourceDirectory: './packages/web/dist', 150 | buildCommands: ['npm ci', 'npm run web:build'], 151 | buildEnvironment: { 152 | VITE_APP_API_ENDPOINT: props.apiEndpointUrl, 153 | VITE_APP_KENDRA_INDEX_ID: props.kendraIndexId, 154 | VITE_APP_REGION: Stack.of(this).region, 155 | VITE_APP_USER_POOL_ID: props.userPoolId, 156 | VITE_APP_USER_POOL_CLIENT_ID: props.userPoolClientId, 157 | VITE_APP_IDENTITY_POOL_ID: props.idPoolId, 158 | VITE_APP_PREDICT_STREAM_FUNCTION_ARN: props.predictStreamFunctionArn, 159 | VITE_APP_SELF_SIGN_UP_ENABLED: props.selfSignUpEnabled.toString(), 160 | VITE_APP_MODEL_REGION: props.modelRegion, 161 | VITE_APP_MODEL_IDS: JSON.stringify(props.modelIds), 162 | VITE_APP_SAMLAUTH_ENABLED: props.samlAuthEnabled.toString(), 163 | VITE_APP_SAML_COGNITO_DOMAIN_NAME: 164 | props.samlCognitoDomainName.toString(), 165 | VITE_APP_SAML_COGNITO_FEDERATED_IDENTITY_PROVIDER_NAME: 166 | props.samlCognitoFederatedIdentityProviderName.toString(), 167 | }, 168 | }); 169 | 170 | this.distribution = cloudFrontWebDistribution; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /packages/cdk/lib/jp-rag-sample-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Auth, Api, Web, Rag, CommonWebAcl } from './construct'; 4 | import { CfnWebACLAssociation } from 'aws-cdk-lib/aws-wafv2'; 5 | import * as cognito from 'aws-cdk-lib/aws-cognito'; 6 | import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; 7 | 8 | const errorMessageForBooleanContext = (key: string) => { 9 | return `${key} の設定でエラーになりました。原因として考えられるものは以下です。 10 | - cdk.json の変更ではなく、-c オプションで設定しようとしている 11 | - cdk.json に boolean ではない値を設定している (例: "true" ダブルクォートは不要) 12 | - cdk.json に項目がない (未設定)`; 13 | }; 14 | 15 | interface JpRagSampleStackProps extends StackProps { 16 | webAclId?: string; 17 | allowedIpV4AddressRanges: string[] | null; 18 | allowedIpV6AddressRanges: string[] | null; 19 | allowedCountryCodes: string[] | null; 20 | vpcId?: string; 21 | cert?: ICertificate; 22 | hostName?: string; 23 | domainName?: string; 24 | hostedZoneId?: string; 25 | } 26 | 27 | export class JpRagSampleStack extends Stack { 28 | public readonly userPool: cognito.UserPool; 29 | public readonly userPoolClient: cognito.UserPoolClient; 30 | 31 | constructor(scope: Construct, id: string, props: JpRagSampleStackProps) { 32 | super(scope, id, props); 33 | 34 | const selfSignUpEnabled: boolean = 35 | this.node.tryGetContext('selfSignUpEnabled')!; 36 | const allowedSignUpEmailDomains: string[] | null | undefined = 37 | this.node.tryGetContext('allowedSignUpEmailDomains'); 38 | const samlAuthEnabled: boolean = 39 | this.node.tryGetContext('samlAuthEnabled')!; 40 | const samlCognitoDomainName: string = this.node.tryGetContext( 41 | 'samlCognitoDomainName' 42 | )!; 43 | const samlCognitoFederatedIdentityProviderName: string = 44 | this.node.tryGetContext('samlCognitoFederatedIdentityProviderName')!; 45 | 46 | if (typeof selfSignUpEnabled !== 'boolean') { 47 | throw new Error(errorMessageForBooleanContext('selfSignUpEnabled')); 48 | } 49 | 50 | if (typeof samlAuthEnabled !== 'boolean') { 51 | throw new Error(errorMessageForBooleanContext('samlAuthEnabled')); 52 | } 53 | 54 | const auth = new Auth(this, 'Auth', { 55 | selfSignUpEnabled, 56 | allowedIpV4AddressRanges: props.allowedIpV4AddressRanges, 57 | allowedIpV6AddressRanges: props.allowedIpV6AddressRanges, 58 | allowedSignUpEmailDomains, 59 | samlAuthEnabled, 60 | }); 61 | 62 | const api = new Api(this, 'API', { 63 | userPool: auth.userPool, 64 | idPool: auth.idPool, 65 | }); 66 | 67 | const rag = new Rag(this, 'Rag', { 68 | userPool: auth.userPool, 69 | api: api.api, 70 | }); 71 | 72 | if ( 73 | props.allowedIpV4AddressRanges || 74 | props.allowedIpV6AddressRanges || 75 | props.allowedCountryCodes 76 | ) { 77 | const regionalWaf = new CommonWebAcl(this, 'RegionalWaf', { 78 | scope: 'REGIONAL', 79 | allowedIpV4AddressRanges: props.allowedIpV4AddressRanges, 80 | allowedIpV6AddressRanges: props.allowedIpV6AddressRanges, 81 | allowedCountryCodes: props.allowedCountryCodes, 82 | }); 83 | new CfnWebACLAssociation(this, 'ApiWafAssociation', { 84 | resourceArn: api.api.deploymentStage.stageArn, 85 | webAclArn: regionalWaf.webAclArn, 86 | }); 87 | new CfnWebACLAssociation(this, 'UserPoolWafAssociation', { 88 | resourceArn: auth.userPool.userPoolArn, 89 | webAclArn: regionalWaf.webAclArn, 90 | }); 91 | } 92 | 93 | const web = new Web(this, 'Api', { 94 | apiEndpointUrl: api.api.url, 95 | kendraIndexId: rag.kendraIndexId, 96 | userPoolId: auth.userPool.userPoolId, 97 | userPoolClientId: auth.client.userPoolClientId, 98 | idPoolId: auth.idPool.identityPoolId, 99 | predictStreamFunctionArn: api.predictStreamFunction.functionArn, 100 | selfSignUpEnabled, 101 | webAclId: props.webAclId, 102 | modelRegion: api.modelRegion, 103 | modelIds: api.modelIds, 104 | samlAuthEnabled, 105 | samlCognitoDomainName, 106 | samlCognitoFederatedIdentityProviderName, 107 | cert: props.cert, 108 | hostName: props.hostName, 109 | domainName: props.domainName, 110 | hostedZoneId: props.hostedZoneId, 111 | }); 112 | 113 | new CfnOutput(this, 'Region', { 114 | value: this.region, 115 | }); 116 | 117 | if (props.hostName && props.domainName) { 118 | new CfnOutput(this, 'WebUrl', { 119 | value: `https://${props.hostName}.${props.domainName}`, 120 | }); 121 | } else { 122 | new CfnOutput(this, 'WebUrl', { 123 | value: `https://${web.distribution.domainName}`, 124 | }); 125 | } 126 | 127 | new CfnOutput(this, 'ApiEndpoint', { 128 | value: api.api.url, 129 | }); 130 | 131 | new CfnOutput(this, 'KendraIndexId', { 132 | value: rag.kendraIndexId, 133 | }); 134 | 135 | new CfnOutput(this, 'UserPoolId', { value: auth.userPool.userPoolId }); 136 | 137 | new CfnOutput(this, 'UserPoolClientId', { 138 | value: auth.client.userPoolClientId, 139 | }); 140 | 141 | new CfnOutput(this, 'IdPoolId', { value: auth.idPool.identityPoolId }); 142 | 143 | new CfnOutput(this, 'PredictStreamFunctionArn', { 144 | value: api.predictStreamFunction.functionArn, 145 | }); 146 | 147 | new CfnOutput(this, 'SelfSignUpEnabled', { 148 | value: selfSignUpEnabled.toString(), 149 | }); 150 | 151 | new CfnOutput(this, 'ModelRegion', { 152 | value: api.modelRegion, 153 | }); 154 | 155 | new CfnOutput(this, 'ModelIds', { 156 | value: JSON.stringify(api.modelIds), 157 | }); 158 | 159 | new CfnOutput(this, 'SamlAuthEnabled', { 160 | value: samlAuthEnabled.toString(), 161 | }); 162 | 163 | new CfnOutput(this, 'SamlCognitoDomainName', { 164 | value: samlCognitoDomainName.toString(), 165 | }); 166 | 167 | new CfnOutput(this, 'SamlCognitoFederatedIdentityProviderName', { 168 | value: samlCognitoFederatedIdentityProviderName.toString(), 169 | }); 170 | 171 | this.userPool = auth.userPool; 172 | this.userPoolClient = auth.client; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /packages/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "private": true, 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "cdk": "cdk", 8 | "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0" 9 | }, 10 | "devDependencies": { 11 | "@types/aws-lambda": "^8.10.137", 12 | "@types/node": "20.12.7", 13 | "@types/cfn-response": "^1.0.8", 14 | "@typescript-eslint/eslint-plugin": "^7.10.0", 15 | "@typescript-eslint/parser": "^7.10.0", 16 | "aws-cdk": "2.139.0", 17 | "eslint": "^8.56.0", 18 | "ts-node": "^10.9.2", 19 | "typescript": "~5.4.5" 20 | }, 21 | "dependencies": { 22 | "@aws-cdk/aws-cognito-identitypool-alpha": "^2.22.0-alpha.0", 23 | "@aws-cdk/aws-lambda-python-alpha": "2.139.0-alpha.0", 24 | "@aws-sdk/client-bedrock-runtime": "^3.549.0", 25 | "@aws-sdk/client-kendra": "^3.549.0", 26 | "@aws-sdk/client-s3": "^3.549.0", 27 | "@aws-sdk/s3-request-presigner": "^3.549.0", 28 | "@aws-solutions-constructs/aws-cloudfront-s3": "^2.54.1", 29 | "aws-cdk-lib": "2.139.0", 30 | "constructs": "^10.0.0", 31 | "deploy-time-build": "^0.3.17", 32 | "source-map-support": "^0.5.21", 33 | "cfn-response": "^1.0.1" 34 | } 35 | } -------------------------------------------------------------------------------- /packages/cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@types/jp-rag-sample", 3 | "types": "src/index.d.ts", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-sdk/client-kendra": "^3.549.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/types/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './message'; 2 | export * from './protocol'; 3 | export * from './text'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /packages/types/src/message.d.ts: -------------------------------------------------------------------------------- 1 | export type Role = 'system' | 'user' | 'assistant'; 2 | 3 | export type Model = { 4 | type: 'bedrock' | 'bedrockAgent' | 'sagemaker'; 5 | modelId: string; 6 | sessionId?: string; 7 | }; 8 | 9 | export type UnrecordedMessage = { 10 | role: Role; 11 | content: string; 12 | llmType?: string; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/types/src/protocol.d.ts: -------------------------------------------------------------------------------- 1 | import { Model, UnrecordedMessage } from './message'; 2 | import { 3 | QueryCommandOutput, 4 | RetrieveCommandOutput, 5 | } from '@aws-sdk/client-kendra'; 6 | 7 | export type PredictRequest = { 8 | model?: Model; 9 | messages: UnrecordedMessage[]; 10 | id: string; 11 | }; 12 | 13 | export type QueryKendraRequest = { 14 | query: string; 15 | }; 16 | 17 | export type QueryKendraResponse = QueryCommandOutput; 18 | 19 | export type RetrieveKendraRequest = { 20 | query: string; 21 | }; 22 | 23 | export type RetrieveKendraResponse = RetrieveCommandOutput; 24 | 25 | export type GetDocDownloadSignedUrlRequest = { 26 | bucketName: string; 27 | filePrefix: string; 28 | contentType?: string; 29 | }; 30 | 31 | export type GetDocDownloadSignedUrlResponse = string; 32 | -------------------------------------------------------------------------------- /packages/types/src/text.d.ts: -------------------------------------------------------------------------------- 1 | // Claude Message 2 | // https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html 3 | export type ClaudeMessageParams = { 4 | system?: string; 5 | anthropic_version?: string; 6 | messages?: { 7 | role: string; 8 | content: { 9 | type: string; 10 | text?: string; 11 | source?: { 12 | type: string; 13 | media_type: string; 14 | data: string; 15 | }; 16 | }[]; 17 | }[]; 18 | max_tokens?: number; 19 | stop_sequences?: string[]; 20 | temperature?: number; 21 | top_k?: number; 22 | top_p?: number; 23 | }; 24 | 25 | export type BedrockResponse = { 26 | // Claude 27 | completion: string; 28 | type: string; 29 | // ClaudeMessage Non-stream 30 | content: { 31 | type: string; 32 | text: string; 33 | }[]; 34 | // ClaudeMessage Stream 35 | delta: { 36 | text: string; 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/types/src/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { Model, UnrecordedMessage } from 'jp-rag-sample'; 2 | 3 | export type InvokeInterface = ( 4 | model: Model, 5 | messages: UnrecordedMessage[], 6 | id: string 7 | ) => Promise; 8 | 9 | export type InvokeStreamInterface = ( 10 | model: Model, 11 | messages: UnrecordedMessage[], 12 | id: string 13 | ) => AsyncIterable; 14 | 15 | export type ApiInterface = { 16 | invoke: InvokeInterface; 17 | invokeStream: InvokeStreamInterface; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | // TODO: any を削除 18 | '@typescript-eslint/no-explicit-any': 'off', 19 | '@typescript-eslint/no-inferrable-types': 'off', 20 | }, 21 | settings: {}, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dev-dist 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? -------------------------------------------------------------------------------- /packages/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JP RAG Sample 8 | 9 | 10 |
11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rag-kendra", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@aws-amplify/ui-react": "^5.0.1", 14 | "@aws-sdk/client-cognito-identity": "^3.577.0", 15 | "@aws-sdk/client-kendra": "^3.334.0", 16 | "@aws-sdk/client-lambda": "^3.577.0", 17 | "@aws-sdk/credential-provider-cognito-identity": "^3.577.0", 18 | "@chakra-ui/icons": "^2.0.19", 19 | "@chakra-ui/react": "^2.6.1", 20 | "@emotion/react": "^11.11.0", 21 | "@emotion/styled": "^11.11.0", 22 | "@types/node": "^20.1.7", 23 | "aws-amplify": "^5.3.12", 24 | "framer-motion": "^10.12.11", 25 | "i18next": "^23.5.1", 26 | "qrcode": "^1.5.3", 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-router-dom": "^6.22.3", 30 | "react-i18next": "^13.2.2", 31 | "react-icons": "^4.8.0", 32 | "recast": "0.21.0" 33 | }, 34 | "devDependencies": { 35 | "@types/qrcode": "^1.5.1", 36 | "@types/react": "^18.0.28", 37 | "@types/react-dom": "^18.0.11", 38 | "@typescript-eslint/eslint-plugin": "^5.57.1", 39 | "@typescript-eslint/parser": "^5.57.1", 40 | "@vitejs/plugin-react": "^4.0.0", 41 | "eslint": "^8.57.0", 42 | "eslint-plugin-react-hooks": "^4.6.0", 43 | "eslint-plugin-react-refresh": "^0.4.6", 44 | "typescript": "^5.0.2", 45 | "vite": "^4.5.2", 46 | "vite-plugin-pwa": "^0.19.8", 47 | "vite-plugin-node-polyfills": "^0.21.0", 48 | "vite-plugin-svgr": "^4.2.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/favicon.png -------------------------------------------------------------------------------- /packages/web/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 28 | 29 | 31 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /packages/web/public/images/aws.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/web/public/images/aws_bg_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/web/public/images/aws_icon_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/images/aws_icon_180.png -------------------------------------------------------------------------------- /packages/web/public/images/aws_icon_180_bg_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/images/aws_icon_180_bg_white.png -------------------------------------------------------------------------------- /packages/web/public/images/aws_icon_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/images/aws_icon_192.png -------------------------------------------------------------------------------- /packages/web/public/images/aws_icon_192_maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/images/aws_icon_192_maskable.png -------------------------------------------------------------------------------- /packages/web/public/images/aws_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/images/aws_icon_512.png -------------------------------------------------------------------------------- /packages/web/public/images/aws_icon_512_maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/jp-rag-sample/9d08bddacbe8c7a2086f18cdb6deff7cbc841c78/packages/web/public/images/aws_icon_512_maskable.png -------------------------------------------------------------------------------- /packages/web/public/images/aws_maskable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/web/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { useEffect, useState } from 'react'; 5 | import TopBar from './layout/TopBar.tsx'; 6 | import MainArea from './layout/MainArea.tsx'; 7 | import { getSortOrderFromIndex, getDatasourceInfo } from './utils/service.ts'; 8 | import { 9 | AiAgentHistory, 10 | Conversation, 11 | Dic, 12 | Filter, 13 | } from './utils/interface.tsx'; 14 | import { DEFAULT_LANGUAGE } from './utils/constant.tsx'; 15 | import { GlobalContext } from './utils/globalContext'; 16 | // Amplify 17 | import { useAuthenticator } from '@aws-amplify/ui-react'; 18 | import '@aws-amplify/ui-react/styles.css'; 19 | // i18 20 | import './i18n/configs.ts'; 21 | 22 | function App() { 23 | const { user, signOut } = useAuthenticator((context) => [context.user]); 24 | const [currentConversation, setCurrentConversation] = useState< 25 | Conversation | undefined 26 | >(); // 現在の結果 27 | const [filterOptions, setFilterOptions] = useState([]); // 現在適用中のフィルタ 28 | const [datasourceInfo, setDatasourceInfo] = useState({}); // データソース情報 29 | const [currentInputText, setCurrentInputText] = useState(''); // 入力中の文字列 30 | // const [loginSucceeded, setLoginSucceeded] = useState(false); // ログイン完了フラグ 31 | const [recentQueryList, setRecentQueryList] = useState([]); // 直近のクエリ 32 | // 検索履歴ID 33 | const [currentQueryId, setCurrentQueryId] = useState('initialState'); 34 | // AI Agent の利用履歴 35 | const [aiAgent, setAiAgent] = useState({ 36 | initialState: { 37 | aiAgentResponse: '', 38 | aiSelectedInfoList: [], 39 | suggestedQuery: [], 40 | systemLog: [], 41 | diveDeepIsEnabled: false, 42 | userQuery: '', 43 | }, 44 | }); 45 | 46 | useEffect(() => { 47 | // Stateの初期設定 48 | if (user) { 49 | // 言語設定とソートの候補を取得 50 | const filterOption: Filter[] = [ 51 | { 52 | filterType: 'LAUNGUAGE_SETTING', 53 | title: '言語設定', 54 | options: [], 55 | selected: [DEFAULT_LANGUAGE], 56 | }, 57 | ]; 58 | const getSortOrderFromIndexAndSetSortOption = async () => { 59 | const so = await getSortOrderFromIndex(); 60 | filterOption.push(so); 61 | setFilterOptions(filterOption); 62 | }; 63 | getSortOrderFromIndexAndSetSortOption(); 64 | 65 | const getDataSourceInfo = async () => { 66 | const dsi = await getDatasourceInfo(); 67 | setDatasourceInfo(dsi); 68 | }; 69 | getDataSourceInfo(); 70 | } 71 | }, [user]); 72 | 73 | return ( 74 | 91 | 92 | 93 | 94 | ); 95 | } 96 | 97 | export default App; 98 | -------------------------------------------------------------------------------- /packages/web/src/README.md: -------------------------------------------------------------------------------- 1 | # JP RAG SAMPLE (CLIENT) 2 | 3 | ![](/docs/png/layout.png) 4 | -------------------------------------------------------------------------------- /packages/web/src/components/AuthWithSAML.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import App from '../App.tsx'; 3 | import { Button, Text } from '@aws-amplify/ui-react'; 4 | import { Amplify, Auth } from 'aws-amplify'; 5 | import '@aws-amplify/ui-react/styles.css'; 6 | import { Box, Spinner } from '@chakra-ui/react'; 7 | 8 | const samlCognitoDomainName: string = import.meta.env 9 | .VITE_APP_SAML_COGNITO_DOMAIN_NAME; 10 | const samlCognitoFederatedIdentityProviderName: string = import.meta.env 11 | .VITE_APP_SAML_COGNITO_FEDERATED_IDENTITY_PROVIDER_NAME; 12 | 13 | const AuthWithSAML: React.FC = () => { 14 | const [authenticated, setAuthenticated] = useState(false); 15 | const [loading, setLoading] = useState(true); 16 | 17 | // 未ログインの場合は、ログイン画面を表示する 18 | useEffect(() => { 19 | Auth.currentAuthenticatedUser() 20 | .then(() => { 21 | setAuthenticated(true); 22 | }) 23 | .catch(() => { 24 | setAuthenticated(false); 25 | }) 26 | .finally(() => { 27 | setLoading(false); // 認証チェックが完了したらローディングを終了 28 | }); 29 | }, []); 30 | 31 | const signIn = () => { 32 | Auth.federatedSignIn({ 33 | customProvider: samlCognitoFederatedIdentityProviderName, 34 | }); // cdk.json の値を指定 35 | }; 36 | 37 | Amplify.configure({ 38 | Auth: { 39 | region: import.meta.env.VITE_APP_REGION, 40 | userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, 41 | userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, 42 | identityPoolId: import.meta.env.VITE_APP_IDENTITY_POOL_ID, 43 | oauth: { 44 | domain: samlCognitoDomainName, // cdk.json の値を指定 45 | scope: ['openid', 'email', 'profile', 'aws.cognito.signin.user.admin'], 46 | // CloudFront で展開している Web ページを動的に取得 47 | redirectSignIn: window.location.origin, 48 | redirectSignOut: window.location.origin, 49 | responseType: 'code', 50 | }, 51 | }, 52 | }); 53 | 54 | return ( 55 | <> 56 | {loading ? ( 57 | 62 | 63 | Loading... 64 | 65 | 66 | 67 | ) : !authenticated ? ( 68 | 73 | 74 | JP RAG Sample 75 | 76 | 84 | 85 | ) : ( 86 | 87 | )} 88 | 89 | ); 90 | }; 91 | 92 | export default AuthWithSAML; 93 | -------------------------------------------------------------------------------- /packages/web/src/components/AuthWithUserpool.tsx: -------------------------------------------------------------------------------- 1 | import { Amplify, I18n } from 'aws-amplify'; 2 | import { Authenticator, translations } from '@aws-amplify/ui-react'; 3 | import App from '../App.tsx'; 4 | 5 | const selfSignUpEnabled: boolean = 6 | import.meta.env.VITE_APP_SELF_SIGN_UP_ENABLED === 'true'; 7 | 8 | const AuthWithUserpool: React.FC = () => { 9 | Amplify.configure({ 10 | Auth: { 11 | userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, 12 | userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, 13 | identityPoolId: import.meta.env.VITE_APP_IDENTITY_POOL_ID, 14 | authenticationFlowType: 'USER_SRP_AUTH', 15 | }, 16 | }); 17 | 18 | I18n.putVocabularies(translations); 19 | I18n.setLanguage('ja'); 20 | 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default AuthWithUserpool; 29 | -------------------------------------------------------------------------------- /packages/web/src/i18n/configs.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import i18n from 'i18next'; 5 | import { initReactI18next } from 'react-i18next'; 6 | 7 | // 言語jsonファイルのimport 8 | import translation_en from './en.json'; 9 | import translation_ja from './ja.json'; 10 | 11 | const resources = { 12 | en: { 13 | translation: translation_en, 14 | }, 15 | es: { 16 | translation: translation_en, 17 | }, 18 | fr: { 19 | translation: translation_en, 20 | }, 21 | de: { 22 | translation: translation_en, 23 | }, 24 | pt: { 25 | translation: translation_en, 26 | }, 27 | ja: { 28 | translation: translation_ja, 29 | }, 30 | ko: { 31 | translation: translation_en, 32 | }, 33 | zh: { 34 | translation: translation_en, 35 | }, 36 | it: { 37 | translation: translation_en, 38 | }, 39 | hi: { 40 | translation: translation_en, 41 | }, 42 | ar: { 43 | translation: translation_en, 44 | }, 45 | hy: { 46 | translation: translation_en, 47 | }, 48 | eu: { 49 | translation: translation_en, 50 | }, 51 | bn: { 52 | translation: translation_en, 53 | }, 54 | 'pt-BR': { 55 | translation: translation_en, 56 | }, 57 | bg: { 58 | translation: translation_en, 59 | }, 60 | ca: { 61 | translation: translation_en, 62 | }, 63 | cs: { 64 | translation: translation_en, 65 | }, 66 | da: { 67 | translation: translation_en, 68 | }, 69 | nl: { 70 | translation: translation_en, 71 | }, 72 | fi: { 73 | translation: translation_en, 74 | }, 75 | gl: { 76 | translation: translation_en, 77 | }, 78 | el: { 79 | translation: translation_en, 80 | }, 81 | hu: { 82 | translation: translation_en, 83 | }, 84 | id: { 85 | translation: translation_en, 86 | }, 87 | ga: { 88 | translation: translation_en, 89 | }, 90 | lv: { 91 | translation: translation_en, 92 | }, 93 | lt: { 94 | translation: translation_en, 95 | }, 96 | no: { 97 | translation: translation_en, 98 | }, 99 | fa: { 100 | translation: translation_en, 101 | }, 102 | ro: { 103 | translation: translation_en, 104 | }, 105 | ru: { 106 | translation: translation_en, 107 | }, 108 | ckb: { 109 | translation: translation_en, 110 | }, 111 | sv: { 112 | translation: translation_en, 113 | }, 114 | tr: { 115 | translation: translation_en, 116 | }, 117 | }; 118 | 119 | i18n 120 | .use(initReactI18next) // passes i18n down to react-i18next 121 | .init({ 122 | resources, 123 | lng: 'ja', 124 | interpolation: { 125 | escapeValue: false, // react already safes from xss 126 | }, 127 | }); 128 | 129 | export default i18n; 130 | -------------------------------------------------------------------------------- /packages/web/src/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "top_bar" : { 3 | "account": "account", 4 | "set_mfa": "MFA setting", 5 | "logout": "logout", 6 | "search": "Search" 7 | }, 8 | "left_side_bar" : { 9 | "filter_option_name": { 10 | "lang_setting": "lauguage setting", 11 | "sort_order": "sort order", 12 | "authors": "authors", 13 | "data_source_id": "datasource", 14 | "document_body": "document body", 15 | "document_id": "document id", 16 | "category": "category", 17 | "view_count": "view count", 18 | "created_at": "created at", 19 | "document_title": "document_title", 20 | "excerpt_page_number": "excerpt page number", 21 | "faq_id": "FAQ ID", 22 | "file_type": "File type", 23 | "language_code": "Lauguage", 24 | "last_updated_at": "last updated", 25 | "source_uri": "source URL", 26 | "tenant_id": "tenant ID", 27 | "version": "version" 28 | }, 29 | "parts" : { 30 | "included_string": "include following string", 31 | "start_date": "start", 32 | "end_date": "end", 33 | "apply": "apply" 34 | } 35 | }, 36 | "body" : { 37 | "query": "query", 38 | "related_sentence": "Related sentence", 39 | "faq": "FAQ", 40 | "featured_result": "Featured Result", 41 | "excerpt_result": "Excerpt Result", 42 | "no_result": "no result", 43 | "you": "YOU", 44 | "ai_response": "AI RESPONSE", 45 | "thinking": "thinking..." 46 | }, 47 | "toast" : { 48 | "mfa_success": "MFA was set", 49 | "invalid_filter": "Error (invalid filter)", 50 | "fail_kendra": "Error (fail to call kendra)", 51 | "thanks_feedback": "Thank you for your feedback" 52 | }, 53 | "right_side_bar": { 54 | "ai_agent": "AI Agent", 55 | "quote": "Quote", 56 | "suggest_query": "Suggested Next Query", 57 | "dive_deep_button": "Dive Deep" 58 | } 59 | } -------------------------------------------------------------------------------- /packages/web/src/i18n/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "top_bar" : { 3 | "account": "アカウント", 4 | "set_mfa": "MFA の設定", 5 | "logout": "ログアウト", 6 | "search": "検索" 7 | }, 8 | "left_side_bar" : { 9 | "filter_option_name": { 10 | "lang_setting": "言語設定", 11 | "sort_order": "並び順", 12 | "authors": "著者", 13 | "data_source_id": "データソース", 14 | "document_body": "ドキュメント本文", 15 | "document_id": "ドキュメント ID", 16 | "category": "カテゴリ", 17 | "view_count": "ビュー数", 18 | "created_at": "作成日", 19 | "document_title": "ドキュメントタイトル", 20 | "excerpt_page_number": "抜粋ページ番号", 21 | "faq_id": "FAQ ID", 22 | "file_type": "ファイルタイプ", 23 | "language_code": "言語", 24 | "last_updated_at": "最終更新日", 25 | "source_uri": "ソース URL", 26 | "tenant_id": "テナント ID", 27 | "version": "バージョン" 28 | }, 29 | "parts" : { 30 | "included_string": "の文字列を含む", 31 | "start_date": "開始", 32 | "end_date": "終了", 33 | "apply": "適用" 34 | } 35 | }, 36 | "body" : { 37 | "query": "クエリ", 38 | "related_sentence": "関連する文章", 39 | "faq": "FAQ", 40 | "featured_result": "管理者によって強調された文章", 41 | "excerpt_result": "抜粋された文章", 42 | "no_result": "該当なし", 43 | "you": "あなた", 44 | "ai_response": "AI の回答", 45 | "thinking": "考え中..." 46 | }, 47 | "toast" : { 48 | "mfa_success": "MFA が設定されました", 49 | "invalid_filter": "エラー (不正なフィルタ)", 50 | "fail_kendra": "エラー (kendraへの問い合わせに失敗しました)", 51 | "thanks_feedback": "フィードバックありがとうございます" 52 | }, 53 | "right_side_bar": { 54 | "ai_agent": "AI エージェント", 55 | "quote": "引用元", 56 | "suggest_query": "検索サジェスト", 57 | "dive_deep_button": "さらに深堀り" 58 | } 59 | } -------------------------------------------------------------------------------- /packages/web/src/layout/AiArea.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { Button, Flex } from '@chakra-ui/react'; 5 | import { VStack } from '@chakra-ui/layout'; 6 | import { useGlobalContext } from '../utils/useGlobalContext'; 7 | import { 8 | Accordion, 9 | AccordionItem, 10 | Box, 11 | HStack, 12 | Text, 13 | Link, 14 | } from '@chakra-ui/react'; 15 | import { getKendraQuery, kendraQuery, infClaude } from '../utils/service'; 16 | import { 17 | getCurrentSortOrder, 18 | getAttributeFilter, 19 | kendraResultToAiSelectedInfo, 20 | createQuotePrompt, 21 | parseAnswerFromGeneratedQuotes, 22 | createFinalAnswerPrompt, 23 | } from '../utils/function'; 24 | import { AiSelectedInfo } from '../utils/interface'; 25 | // i18 26 | import { useTranslation } from 'react-i18next'; 27 | import { ChatIcon, SearchIcon } from '@chakra-ui/icons'; 28 | 29 | export default function AiArea() { 30 | // 画面中央の表示 31 | // 言語設定 32 | const { t } = useTranslation(); 33 | 34 | const { 35 | filterOptions: filterOptions, 36 | currentInputText: currentInputText, 37 | currentQueryId: currentQueryId, 38 | aiAgent: aiAgent, 39 | setAiAgent: setAiAgent, 40 | } = useGlobalContext(); 41 | 42 | return ( 43 | 44 | 45 | 50 |

51 | 52 | {t('right_side_bar.ai_agent')} 53 | 54 |

55 | 56 | 57 | {/* 回答 */} 58 | 59 | 60 | 61 | 62 | {currentInputText} 63 | 64 | 65 | 66 | {aiAgent[currentQueryId].aiAgentResponse 67 | ?.split('\n') 68 | .map((item, idx) => { 69 | return ( 70 |

71 | {item} 72 |
73 |

74 | ); 75 | })} 76 |
77 |
78 |
79 |
80 | 81 | {/* 引用元 */} 82 | 83 | 84 | 85 | {t('right_side_bar.quote')} 86 | 87 | 88 | 89 | {aiAgent[currentQueryId].aiSelectedInfoList.map( 90 | (item, idx) => { 91 | return ( 92 | 93 | 94 | [{idx}] {item.title} 95 | 96 | 97 | ); 98 | } 99 | )} 100 | 101 | 102 | 103 | 104 | 105 | {/* 検索サジェスト */} 106 | 107 | 108 | 109 | 110 | {t('right_side_bar.suggest_query')} 111 | 112 | 113 | 114 | {aiAgent[currentQueryId].suggestedQuery.map( 115 | (item, idx) => { 116 | return - {item}; 117 | } 118 | )} 119 | 120 | 121 | 122 | 123 | 124 | {/* 深堀り */} 125 | 126 | 238 | 239 |
240 |
241 |
242 |
243 |
244 | ); 245 | } 246 | -------------------------------------------------------------------------------- /packages/web/src/layout/InputWithSuggest.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { HStack, Input, InputGroup, Text, useToast } from '@chakra-ui/react'; 5 | import React, { useState, useRef } from 'react'; 6 | import { AiOutlineBulb, AiOutlineFieldTime } from 'react-icons/ai'; 7 | import { useGlobalContext } from '../utils/useGlobalContext'; 8 | import { 9 | getKendraQuery, 10 | infStreamClaude, 11 | kendraQuery, 12 | } from '../utils/service.ts'; 13 | import { 14 | getAttributeFilter, 15 | getCurrentSortOrder, 16 | getFiltersFromQuery, 17 | createFinalAnswerPrompt, 18 | createNextQeuryPrompt, 19 | createQuotePrompt, 20 | kendraResultToAiSelectedInfo, 21 | parseAnswerFromGeneratedQuotes, 22 | parseNextQueryFromSuggestion, 23 | } from '../utils/function'; 24 | import { 25 | AiAgentStatus, 26 | AiSelectedInfo, 27 | Conversation, 28 | } from '../utils/interface'; 29 | import { 30 | MAX_QUERY_SUGGESTION, 31 | RECENT_QUERY_CAPACITY, 32 | TOP_QUERIES, 33 | } from '../utils/constant'; 34 | import { QueryResult } from '@aws-sdk/client-kendra'; 35 | 36 | // i18 37 | import { useTranslation } from 'react-i18next'; 38 | 39 | const InputWithSuggest: React.FC = () => { 40 | // 言語設定 41 | const { t } = useTranslation(); 42 | 43 | // global変数 44 | const { 45 | currentConversation: currentConversation, 46 | setCurrentConversation: setCurrentConversation, 47 | filterOptions: filterOptions, 48 | setFilterOptions: setFilterOptions, 49 | datasourceInfo: datasourceInfo, 50 | currentInputText: currentInputText, 51 | setCurrentInputText: setCurrentInputText, 52 | recentQueryList: recentQueryList, 53 | setRecentQueryList: setRecentQueryList, 54 | setCurrentQueryId: setCurrentQueryId, 55 | setAiAgent: setAiAgent, 56 | } = useGlobalContext(); 57 | 58 | // local 変数 59 | const [suggestedQueryOptions, setFilteredOptions] = useState( 60 | TOP_QUERIES.slice(0, MAX_QUERY_SUGGESTION) 61 | ); // AIによるサジェストクエリ 62 | const [showOptions, setShowOptions] = useState(false); // サジェストの表示状態 63 | const [currentFocus, setCurrentFocus] = useState(-1); // 選択されたサジェスト位置 64 | const inputRef = useRef(null); // 入力フィールドのDOM 65 | const timeoutId = useRef(null); // サジェストの非表示を制御するためのtimeout 66 | const toast = useToast(); // トースト表示 67 | 68 | const kendraSearch = async (queryText: string): Promise => { 69 | // currentInputTextだと、useStateフックが更新される前に search 処理が走ることになるため、引数にqueryTextを取る 70 | 71 | /* 72 | * Kendraへのリクエスト 73 | * 74 | * 75 | * K. top barからの検索時 76 | * 77 | * K-1. 今 Interaction Areaに何かが表示されている場合は、historyに退避 78 | * K-2. フィルタをリセット 79 | * K-3. 現在設定中のfilterは見ずに、言語設定とソート順序だけを反映させてKendraへQuery 80 | * K-4. 受け取ったレスポンスを元にInteractionAreaを描画 81 | * K-5. Query結果からフィルタ候補を取得 82 | * K-6. FilterBarの設定とソート順序以外を更新 83 | */ 84 | 85 | // K-1. 今 Interaction Areaに何かが表示されている場合は、historyに退避 86 | if (currentConversation !== undefined) { 87 | setCurrentConversation(undefined); 88 | } 89 | // K-2. フィルタをリセット 90 | setFilterOptions([ 91 | filterOptions[0], // 言語設定 92 | filterOptions[1], // ソート順序 93 | ]); 94 | // K-3. 現在設定中のfilterは見ずに、言語設定とソート順序だけを反映させてKendraへQuery 95 | const q = await getKendraQuery( 96 | queryText, 97 | getAttributeFilter(filterOptions), 98 | getCurrentSortOrder(filterOptions) 99 | ); 100 | 101 | try { 102 | const data = await kendraQuery(q); 103 | const a: Conversation = { 104 | conversationType: 'HUMAN_KENDRA', 105 | userInput: { word: queryText }, 106 | userQuery: q, 107 | kendraResponse: data, 108 | aiResponse: undefined, 109 | }; 110 | // K-4. 受け取ったレスポンスを元にInteractionAreaを描画 111 | setCurrentConversation(a); 112 | // K-5. Query結果からフィルタ候補を取得 113 | // K-6. FilterBarの設定とソート順序以外を更新 114 | if (data) { 115 | setFilterOptions([ 116 | filterOptions[0], // 言語設定 117 | filterOptions[1], // ソート順序 118 | ...getFiltersFromQuery(data, datasourceInfo), 119 | ]); // クエリから受け取ったフィルタ候補 120 | } 121 | return data; 122 | } catch (err) { 123 | console.log(err); 124 | toast({ 125 | title: t('toast.fail_kendra'), 126 | description: '', 127 | status: 'error', 128 | duration: 1000, 129 | isClosable: true, 130 | }); 131 | throw err; 132 | } 133 | }; 134 | 135 | const research = async (queryText: string) => { 136 | /** 137 | * Kendra と genAI で調査 138 | */ 139 | 140 | // 現在時刻を取得 141 | const currentTime = new Date().getTime(); 142 | const queryId = `${currentTime}-${queryText}`; 143 | 144 | // aiAgentに新しいモックデータを入れる 145 | const mockAiAgentStatus: AiAgentStatus = { 146 | aiAgentResponse: '', 147 | aiSelectedInfoList: [], 148 | suggestedQuery: [], 149 | systemLog: [], 150 | diveDeepIsEnabled: false, 151 | userQuery: queryText, 152 | }; 153 | setAiAgent((prev) => ({ 154 | ...prev, 155 | [queryId]: mockAiAgentStatus, 156 | })); 157 | 158 | // currentQueryIdを設定 159 | setCurrentQueryId(queryId); 160 | 161 | // 検索履歴として追加 162 | setRecentQueryList((prevList) => { 163 | // prevListの0番目の要素がvalueと同じなら変更しない 164 | if (prevList[0] === queryText) { 165 | return prevList; 166 | } 167 | // RECENT_QUERY_CAPACITY より大きい場合、prevListの最後の要素を削除 168 | if (prevList.length >= RECENT_QUERY_CAPACITY) { 169 | prevList.pop(); 170 | } 171 | return [queryText, ...prevList]; 172 | }); 173 | 174 | // 検索 175 | const kendraResult = await kendraSearch(queryText); 176 | 177 | console.log('kendraResult'); 178 | console.log(kendraResult); 179 | const parsedResult = kendraResultToAiSelectedInfo(kendraResult); 180 | console.log('parsedResult'); 181 | console.log(parsedResult); 182 | 183 | // 検索後 サジェストを再表示 184 | setShowOptions(false); 185 | 186 | // 引用候補を生成 187 | const streamQuote = await infStreamClaude( 188 | createQuotePrompt(parsedResult, queryText) 189 | ); 190 | let tmpResultQuote = ''; 191 | for await (const chunk of streamQuote) { 192 | tmpResultQuote += chunk; 193 | setAiAgent((prev) => { 194 | return { 195 | ...prev, 196 | [queryId]: { 197 | ...prev[queryId], 198 | aiAgentResponse: prev[queryId].aiAgentResponse + chunk, 199 | }, 200 | }; 201 | }); 202 | } 203 | 204 | // 生成した引用を構造化 205 | console.log('tmpResultQuote'); 206 | console.log(tmpResultQuote); 207 | 208 | const parsedAnswer = parseAnswerFromGeneratedQuotes( 209 | '' + tmpResultQuote 210 | ); 211 | const relevantSelectedInfoMap = new Map(); 212 | parsedAnswer.quotes.forEach((quote) => { 213 | const info = parsedResult.find( 214 | (_, index) => index === quote.documentIndex 215 | ); 216 | if (info !== undefined) { 217 | relevantSelectedInfoMap.set(quote.documentIndex, info); 218 | } 219 | }); 220 | const relevantSelectedInfo: AiSelectedInfo[] = Array.from( 221 | relevantSelectedInfoMap.values() 222 | ); 223 | 224 | // 引用を画面描画 225 | console.log('relevantSelectedInfo'); 226 | console.log(relevantSelectedInfo); 227 | setAiAgent((prev) => { 228 | return { 229 | ...prev, 230 | [queryId]: { 231 | ...prev[queryId], 232 | aiSelectedInfoList: relevantSelectedInfo, 233 | }, 234 | }; 235 | }); 236 | 237 | // AI エージェントの吹き出しの内容をリセット 238 | if (relevantSelectedInfo.length <= 0) { 239 | setAiAgent((prev) => { 240 | return { 241 | ...prev, 242 | [queryId]: { 243 | ...prev[queryId], 244 | aiAgentResponse: '関連する文章はみつかりませんでした。', 245 | }, 246 | }; 247 | }); 248 | } else { 249 | setAiAgent((prev) => { 250 | return { 251 | ...prev, 252 | [queryId]: { 253 | ...prev[queryId], 254 | aiAgentResponse: '', 255 | }, 256 | }; 257 | }); 258 | 259 | // 最終回等を生成 260 | console.log('createFinalAnswerPrompt(relevantSelectedInfo, queryText)'); 261 | console.log(createFinalAnswerPrompt(relevantSelectedInfo, queryText)); 262 | 263 | const streamFinalAnswer = await infStreamClaude( 264 | createFinalAnswerPrompt(relevantSelectedInfo, queryText) 265 | ); 266 | // let tmpResultAnswer = ''; 267 | for await (const chunk of streamFinalAnswer) { 268 | // tmpResultAnswer += chunk; 269 | setAiAgent((prev) => { 270 | return { 271 | ...prev, 272 | [queryId]: { 273 | ...prev[queryId], 274 | aiAgentResponse: prev[queryId].aiAgentResponse + chunk, 275 | }, 276 | }; 277 | }); 278 | } 279 | } 280 | 281 | // 次のクエリ候補を作成 282 | console.log('createNextQeuryPrompt(tmpResultQuote, queryText)'); 283 | console.log(createNextQeuryPrompt(tmpResultQuote, queryText)); 284 | 285 | const streamNextQuery = await infStreamClaude( 286 | createNextQeuryPrompt(tmpResultQuote, queryText) 287 | ); 288 | let tmpNextQuery = ''; 289 | for await (const chunk of streamNextQuery) { 290 | tmpNextQuery += chunk; 291 | } 292 | 293 | // 生成されたクエリ候補を構造化 294 | console.log('tmpNextQuery'); 295 | console.log(tmpNextQuery); 296 | 297 | const nextquery = parseNextQueryFromSuggestion(tmpNextQuery); 298 | 299 | // クエリ候補を描画 300 | console.log('nextquery'); 301 | console.log(nextquery); 302 | 303 | setAiAgent((prev) => { 304 | return { 305 | ...prev, 306 | [queryId]: { 307 | ...prev[queryId], 308 | suggestedQuery: nextquery, 309 | diveDeepIsEnabled: true, 310 | }, 311 | }; 312 | }); 313 | }; 314 | 315 | // 入力フィールドの値変更時の挙動 316 | const handleInputChange = (e: React.ChangeEvent) => { 317 | // 入力フィールドの値を取得し記録 318 | const value = e.target.value; 319 | setCurrentInputText(value); 320 | 321 | // 入力があれば、TOP_QUERIES の中から絞り込む、ただし候補数を制限 322 | setFilteredOptions( 323 | TOP_QUERIES.filter((option) => 324 | option.toLowerCase().includes(value.toLowerCase()) 325 | ).slice(0, MAX_QUERY_SUGGESTION) 326 | ); 327 | 328 | setCurrentFocus(-1); 329 | }; 330 | 331 | // サジェスト候補をクリック時の挙動 332 | const handleOptionClick = (value: string) => { 333 | // クリックした候補の値を入力フィールドに代入 334 | setCurrentInputText(value); 335 | 336 | // 検索履歴として追加 337 | setRecentQueryList((prevList) => { 338 | // prevListの0番目の要素がvalueと同じなら変更しない 339 | if (prevList[0] === value) { 340 | return prevList; 341 | } 342 | // RECENT_QUERY_CAPACITY より大きい場合、prevListの最後の要素を削除 343 | if (prevList.length >= RECENT_QUERY_CAPACITY) { 344 | prevList.pop(); 345 | } 346 | return [value, ...prevList]; 347 | }); 348 | // 調査 349 | research(value); 350 | 351 | // サジェストを再表示 352 | setShowOptions(false); 353 | timeoutId.current = setTimeout(() => { 354 | setShowOptions(true); 355 | }, 200); 356 | 357 | // サジェストのフォーカスを外す 358 | setCurrentFocus(-1); 359 | }; 360 | 361 | // 入力フィールド選択時にサジェスト候補を表示 362 | const handleInputFocus = () => { 363 | setShowOptions(true); 364 | }; 365 | 366 | const handleInputBlur = () => { 367 | setCurrentFocus(-1); // サジェストのフォーカスを外す 368 | 369 | // div の onClick より input の onBlur が優先されてしまうので、遅らせる 370 | timeoutId.current = setTimeout(() => { 371 | setShowOptions(false); 372 | }, 200); 373 | }; 374 | 375 | const handleKeyDown = async (e: React.KeyboardEvent) => { 376 | // キーストロークがあった場合はサジェストを表示 377 | setShowOptions(true); 378 | 379 | if ( 380 | e.nativeEvent.isComposing || 381 | (e.key !== 'Enter' && e.key !== 'Tab' && e.key !== 'Escape') 382 | ) 383 | return; 384 | 385 | // エスケープキーが押された場合 386 | if (e.key === 'Escape') { 387 | setShowOptions(false); // サジェストを非表示にする 388 | return; 389 | } 390 | 391 | // タブキーが押された場合 392 | if (e.key === 'Tab') { 393 | e.preventDefault(); // タブキーの標準動作を防ぐ 394 | 395 | // currentFocusの値をローテーション 396 | setCurrentFocus((prevFocus) => { 397 | const totalOptions = 398 | recentQueryList.length + suggestedQueryOptions.length; 399 | if (prevFocus === -1) { 400 | return 0; // 最初の候補を選択 401 | } else if (prevFocus === totalOptions - 1) { 402 | return -1; // 最後の候補から戻る 403 | } else { 404 | return prevFocus + 1; // 次の候補を選択 405 | } 406 | }); 407 | return; 408 | } 409 | let tmpInputText = currentInputText; 410 | 411 | // Enterキーが押された場合 412 | // currentFocusが-1より大きい場合、選択されているサジェスト候補を入力フィールドに設定 413 | if (currentFocus >= 0) { 414 | const selectedOption = 415 | currentFocus < recentQueryList.length 416 | ? recentQueryList[currentFocus] 417 | : suggestedQueryOptions[currentFocus - recentQueryList.length]; 418 | setCurrentInputText(selectedOption); 419 | tmpInputText = selectedOption; 420 | } 421 | 422 | // なにか入力があるときのみ実行 423 | if (tmpInputText === '') { 424 | return; 425 | } 426 | 427 | // 調査 428 | research(tmpInputText); 429 | }; 430 | 431 | return ( 432 | 433 | {/* 入力フィールド */} 434 | 445 | 446 | {/* サジェスト候補 */} 447 | {showOptions && ( 448 |
457 | {/* 履歴によるサジェスト */} 458 | {recentQueryList.map((option, index) => ( 459 | handleOptionClick(option)} 462 | onMouseEnter={() => setCurrentFocus(index)} // マウスカーソルが乗ったものを記録 463 | onMouseLeave={() => setCurrentFocus(-1)} // マウスカーソルが外れたことを記録 464 | style={{ 465 | backgroundColor: 466 | currentFocus === index 467 | ? 'var(--chakra-colors-green-100)' 468 | : 'white', // マウスカーソルが乗ったものの色を変える 469 | cursor: 'pointer', 470 | padding: '5px', 471 | border: '1px solid lightgray', 472 | }}> 473 | 474 | {option} 475 | 476 | ))} 477 | 478 | {/* AIによるサジェスト */} 479 | {suggestedQueryOptions.map((option, index) => ( 480 | handleOptionClick(option)} 483 | onMouseEnter={() => 484 | setCurrentFocus(index + recentQueryList.length) 485 | } // マウスカーソルが乗ったものを記録 486 | onMouseLeave={() => setCurrentFocus(-1)} // マウスカーソルが外れたことを記録 487 | style={{ 488 | backgroundColor: 489 | currentFocus === index + recentQueryList.length 490 | ? 'var(--chakra-colors-green-100)' 491 | : 'white', // マウスカーソルが乗ったものの色を変える 492 | cursor: 'pointer', 493 | padding: '5px', 494 | border: '1px solid lightgray', 495 | }}> 496 | 497 | {option} 498 | 499 | ))} 500 |
501 | )} 502 |
503 | ); 504 | }; 505 | 506 | export default InputWithSuggest; 507 | -------------------------------------------------------------------------------- /packages/web/src/layout/KendraAreaAssets/KendraAreaMain.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { Conversation } from '../../utils/interface'; 5 | import { FeaturedResultsItem, QueryResultItem } from '@aws-sdk/client-kendra'; 6 | import { useEffect, useState } from 'react'; 7 | import { KendraResultFeatured } from './components/KendraResultFeatured'; 8 | import { KendraResultExcerpt } from './components/KendraResultExcerpt'; 9 | import { KendraResultFAQ } from './components/KendraResultFAQ'; 10 | import { KendraResultDoc } from './components/KendraResultDoc'; 11 | 12 | const Kendra: React.FC<{ data: Conversation }> = ({ data }) => { 13 | // Kendraモード, RAGモード時の吹き出し 14 | 15 | const [featuredItems, setFeaturedItems] = useState([]); 16 | const [faqItems, setFaqItems] = useState([]); 17 | const [excerptItems, setExcerptItems] = useState([]); 18 | const [docItems, setDocItems] = useState([]); 19 | 20 | useEffect(() => { 21 | const tmpFeaturedItems: FeaturedResultsItem[] = []; 22 | const tmpFaqItems: QueryResultItem[] = []; 23 | const tmpExcerptItems: QueryResultItem[] = []; 24 | const tmpDocItems: QueryResultItem[] = []; 25 | 26 | // Featured Itemのデータを分離 27 | if (data && data?.kendraResponse?.FeaturedResultsItems) { 28 | for (const result of data.kendraResponse.FeaturedResultsItems) { 29 | tmpFeaturedItems.push(result); 30 | } 31 | } 32 | 33 | // FAQ、抜粋した回答、ドキュメントを分離 34 | if (data && data?.kendraResponse?.ResultItems) { 35 | for (const result of data.kendraResponse.ResultItems) { 36 | switch (result.Type) { 37 | case 'ANSWER': 38 | tmpExcerptItems.push(result); 39 | break; 40 | case 'QUESTION_ANSWER': 41 | tmpFaqItems.push(result); 42 | break; 43 | case 'DOCUMENT': 44 | tmpDocItems.push(result); 45 | break; 46 | default: 47 | break; 48 | } 49 | } 50 | } 51 | 52 | setFeaturedItems(tmpFeaturedItems); 53 | setFaqItems(tmpFaqItems); 54 | setExcerptItems(tmpExcerptItems); 55 | setDocItems(tmpDocItems); 56 | }, [data]); 57 | 58 | return ( 59 | <> 60 | {/* FeaturedResultを表示 */} 61 | 65 | {/* FAQを表示 */} 66 | 70 | {/* 抜粋した回答を表示 */} 71 | 75 | {/* 文章のリストを表示 */} 76 | 80 | 81 | ); 82 | }; 83 | export default Kendra; 84 | -------------------------------------------------------------------------------- /packages/web/src/layout/KendraAreaAssets/components/HighlightedTexts.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import React from 'react'; 5 | import { Highlight, TextWithHighlights } from '@aws-sdk/client-kendra'; 6 | // データの有無を確認 7 | export const isNullOrEmpty = (it: { readonly length: number }) => 8 | isNullOrUndefined(it) || it.length === 0; 9 | export const isNullOrUndefined = (it: any) => it === null || it === undefined; 10 | 11 | export function unionSortedHighlights(highlights: any) { 12 | // highlightの順序を整理 13 | 14 | // highlightがなければそのまま返す 15 | if (isNullOrEmpty(highlights)) { 16 | return highlights; 17 | } 18 | 19 | // highlightの順序を整理 20 | let prev = highlights[0]; 21 | const unioned = [prev]; 22 | for (let i = 1; i < highlights.length; i++) { 23 | const h = highlights[i]; 24 | if (prev.EndOffset >= h.BeginOffset) { 25 | // 前の highlight と次の highlight がつながっている時1つにまとめる 26 | prev.EndOffset = Math.max(h.EndOffset, prev.EndOffset); 27 | } else { 28 | // 結合せず次の highlight を見る 29 | unioned.push(h); 30 | prev = h; 31 | } 32 | } 33 | 34 | return unioned; 35 | } 36 | 37 | class HighlightedText extends React.Component<{ 38 | text: string | undefined; 39 | highlight: Highlight; 40 | }> { 41 | // 重要な文字を切り取って強調 42 | 43 | render() { 44 | const { text, highlight } = this.props; 45 | return ( 46 | 47 | {!isNullOrUndefined(text) && 48 | text?.substring( 49 | highlight.BeginOffset ?? 0, 50 | highlight.EndOffset ?? text?.length 51 | )} 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default class HighlightedTexts extends React.Component<{ 58 | textWithHighlights: TextWithHighlights; 59 | }> { 60 | // 重要な単語を太文字にする部品 61 | 62 | render() { 63 | const { textWithHighlights } = this.props; 64 | 65 | // 文字がない場合なにもしない 66 | if (isNullOrUndefined(textWithHighlights)) { 67 | return null; 68 | } 69 | 70 | // 文字はあるが、highlightするものがない場合、テキストをそのまま表示する 71 | const text = textWithHighlights.Text; 72 | if (isNullOrUndefined(textWithHighlights.Highlights)) { 73 | return {text}; 74 | } 75 | 76 | // Kendra からの response にある Highlight を並び替える 77 | const sortedHighlights = unionSortedHighlights( 78 | textWithHighlights.Highlights?.sort( 79 | (highlight1: any, highlight2: any) => 80 | highlight1.BeginOffset - highlight2.BeginOffset 81 | ) 82 | ); 83 | const lastHighlight = sortedHighlights[sortedHighlights.length - 1]; 84 | 85 | return ( 86 | 87 | {/* 強調しないテキスト+強調するテキストの塊を繰り返し出力する */} 88 | {sortedHighlights.map((highlight: any, idx: number) => ( 89 | 90 | {/* 強調しないテキスト */} 91 | 92 | {text?.substring( 93 | idx === 0 ? 0 : sortedHighlights[idx - 1].EndOffset, 94 | highlight.BeginOffset 95 | )} 96 | 97 | {/* 強調するテキスト */} 98 | 99 | 100 | ))} 101 | {/* 最後の強調しないテキストを出力する */} 102 | 103 | {text?.substring(lastHighlight ? lastHighlight.EndOffset : 0)} 104 | 105 | 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/web/src/layout/KendraAreaAssets/components/KendraResultDoc.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | import { Box, HStack, Heading, VStack } from '@chakra-ui/layout'; 4 | import { Text, useToast } from '@chakra-ui/react'; 5 | import { IconButton } from '@chakra-ui/button'; 6 | import { AiOutlineDislike, AiOutlineLike } from 'react-icons/ai'; 7 | import HighlightedTexts from './HighlightedTexts'; 8 | import { QueryResultItem } from '@aws-sdk/client-kendra'; 9 | import { Relevance, submitFeedback } from '../../../utils/service'; 10 | import { Link } from '@chakra-ui/react'; 11 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 12 | // i18 13 | import { useTranslation } from 'react-i18next'; 14 | 15 | export const KendraResultDoc: React.FC<{ 16 | queryId: string | undefined; 17 | resultItems: QueryResultItem[]; 18 | }> = ({ queryId, resultItems }) => { 19 | // 言語設定 20 | const { t } = useTranslation(); 21 | 22 | const toast = useToast(); 23 | 24 | if (queryId !== undefined && resultItems.length > 0) { 25 | return ( 26 | <> 27 | 28 | 29 | {t('body.related_sentence')} 30 | 31 | 32 | {resultItems.map((resultItem, idx: number) => ( 33 | 34 | 35 | 36 | { 40 | submitFeedback( 41 | Relevance['Click'], 42 | resultItem.Id ?? '', 43 | queryId 44 | ); 45 | }} 46 | isExternal> 47 | 55 | 56 | 57 | 58 | 59 | 67 | 68 | 73 | } 76 | backgroundColor={'transparent'} 77 | onClick={() => { 78 | toast({ 79 | title: t('toast.thanks_feedback'), 80 | description: '', 81 | status: 'success', 82 | duration: 1000, 83 | isClosable: true, 84 | }); 85 | submitFeedback( 86 | Relevance['Relevant'], 87 | resultItem.Id ?? '', 88 | queryId 89 | ); 90 | }} 91 | /> 92 | } 95 | backgroundColor={'transparent'} 96 | onClick={() => { 97 | toast({ 98 | title: t('toast.thanks_feedback'), 99 | description: '', 100 | status: 'success', 101 | duration: 1000, 102 | isClosable: true, 103 | }); 104 | submitFeedback( 105 | Relevance['NotRelevant'], 106 | resultItem.Id ?? '', 107 | queryId 108 | ); 109 | }} 110 | /> 111 | 112 | 113 | 114 | ))} 115 | 116 | ); 117 | } else { 118 | return ( 119 | <> 120 | 121 | 122 | {t('body.related_sentence')} 123 | 124 | 125 | 126 | 127 | {t('body.no_result')} 128 | 129 | 130 | 131 | ); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /packages/web/src/layout/KendraAreaAssets/components/KendraResultExcerpt.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | import { Box, HStack, Text } from '@chakra-ui/layout'; 4 | import { 5 | Accordion, 6 | AccordionButton, 7 | AccordionIcon, 8 | AccordionItem, 9 | AccordionPanel, 10 | Heading, 11 | useToast, 12 | } from '@chakra-ui/react'; 13 | import { IconButton } from '@chakra-ui/button'; 14 | import { AiOutlineDislike, AiOutlineLike } from 'react-icons/ai'; 15 | import HighlightedTexts from './HighlightedTexts'; 16 | import { QueryResultItem } from '@aws-sdk/client-kendra'; 17 | import { Relevance, submitFeedback } from '../../../utils/service'; 18 | import { Link } from '@chakra-ui/react'; 19 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 20 | import { getFAQWithHighlight } from '../../../utils/function'; 21 | // i18 22 | import { useTranslation } from 'react-i18next'; 23 | 24 | export const KendraResultExcerpt: React.FC<{ 25 | queryId: string | undefined; 26 | resultItems: QueryResultItem[]; 27 | }> = ({ queryId, resultItems }) => { 28 | // 言語設定 29 | const { t } = useTranslation(); 30 | 31 | const toast = useToast(); 32 | 33 | if (queryId !== undefined && resultItems.length > 0) { 34 | return ( 35 | 36 | {t('body.excerpt_result')} 37 | 38 | 39 | {resultItems.map((resultItem: QueryResultItem, idx: number) => ( 40 | 41 | 42 | 43 | { 47 | submitFeedback( 48 | Relevance['Click'], 49 | resultItem.Id ?? '', 50 | queryId 51 | ); 52 | }} 53 | isExternal> 54 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 78 | 79 | 84 | } 87 | backgroundColor={'transparent'} 88 | onClick={() => { 89 | toast({ 90 | title: t('toast.thanks_feedback'), 91 | description: '', 92 | status: 'success', 93 | duration: 1000, 94 | isClosable: true, 95 | }); 96 | submitFeedback( 97 | Relevance['Relevant'], 98 | resultItem.Id ?? '', 99 | queryId 100 | ); 101 | }} 102 | /> 103 | } 106 | backgroundColor={'transparent'} 107 | onClick={() => { 108 | toast({ 109 | title: t('toast.thanks_feedback'), 110 | description: '', 111 | status: 'success', 112 | duration: 1000, 113 | isClosable: true, 114 | }); 115 | submitFeedback( 116 | Relevance['NotRelevant'], 117 | resultItem.Id ?? '', 118 | queryId 119 | ); 120 | }} 121 | /> 122 | 123 | 124 | 125 | ))} 126 | 127 | 128 | ); 129 | } else { 130 | return <>; 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /packages/web/src/layout/KendraAreaAssets/components/KendraResultFAQ.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | import { Box, HStack, Text } from '@chakra-ui/layout'; 4 | import { 5 | Accordion, 6 | AccordionButton, 7 | AccordionIcon, 8 | AccordionItem, 9 | AccordionPanel, 10 | useToast, 11 | } from '@chakra-ui/react'; 12 | import { IconButton } from '@chakra-ui/button'; 13 | import { AiOutlineDislike, AiOutlineLike } from 'react-icons/ai'; 14 | import HighlightedTexts from './HighlightedTexts'; 15 | import { QueryResultItem } from '@aws-sdk/client-kendra'; 16 | import { Relevance, submitFeedback } from '../../../utils/service'; 17 | import { Link } from '@chakra-ui/react'; 18 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 19 | import { getFAQWithHighlight } from '../../../utils/function'; 20 | // i18 21 | import { useTranslation } from 'react-i18next'; 22 | 23 | export const KendraResultFAQ: React.FC<{ 24 | queryId: string | undefined; 25 | resultItems: QueryResultItem[]; 26 | }> = ({ queryId, resultItems }) => { 27 | // 言語設定 28 | const { t } = useTranslation(); 29 | 30 | const toast = useToast(); 31 | 32 | if (queryId !== undefined && resultItems.length > 0) { 33 | return ( 34 | 35 | {t('body.faq')} 36 | 37 | 38 | {resultItems.map((resultItem: QueryResultItem, idx: number) => ( 39 | 40 | 41 | { 45 | submitFeedback( 46 | Relevance['Click'], 47 | resultItem.Id ?? '', 48 | queryId 49 | ); 50 | }} 51 | isExternal> 52 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 75 | 76 | 81 | } 84 | backgroundColor={'transparent'} 85 | onClick={() => { 86 | toast({ 87 | title: t('toast.thanks_feedback'), 88 | description: '', 89 | status: 'success', 90 | duration: 1000, 91 | isClosable: true, 92 | }); 93 | submitFeedback( 94 | Relevance['Relevant'], 95 | resultItem.Id ?? '', 96 | queryId 97 | ); 98 | }} 99 | /> 100 | } 103 | backgroundColor={'transparent'} 104 | onClick={() => { 105 | toast({ 106 | title: t('toast.thanks_feedback'), 107 | description: '', 108 | status: 'success', 109 | duration: 1000, 110 | isClosable: true, 111 | }); 112 | submitFeedback( 113 | Relevance['NotRelevant'], 114 | resultItem.Id ?? '', 115 | queryId 116 | ); 117 | }} 118 | /> 119 | 120 | 121 | 122 | ))} 123 | 124 | 125 | ); 126 | } else { 127 | return <>; 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /packages/web/src/layout/KendraAreaAssets/components/KendraResultFeatured.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | import { Box } from '@chakra-ui/layout'; 4 | import { 5 | Text, 6 | Accordion, 7 | AccordionButton, 8 | AccordionIcon, 9 | AccordionItem, 10 | AccordionPanel, 11 | } from '@chakra-ui/react'; 12 | import HighlightedTexts from './HighlightedTexts'; 13 | import { FeaturedResultsItem } from '@aws-sdk/client-kendra'; 14 | import { Relevance, submitFeedback } from '../../../utils/service'; 15 | import { Link } from '@chakra-ui/react'; 16 | import { ExternalLinkIcon } from '@chakra-ui/icons'; 17 | // i18 18 | import { useTranslation } from 'react-i18next'; 19 | 20 | export const KendraResultFeatured: React.FC<{ 21 | queryId: string | undefined; 22 | resultItems: FeaturedResultsItem[]; 23 | }> = ({ queryId, resultItems }) => { 24 | // 言語設定 25 | const { t } = useTranslation(); 26 | 27 | if (queryId !== undefined && resultItems.length > 0) { 28 | return ( 29 | 30 | {t('body.featured_result')} 31 | 32 | 33 | {resultItems.map((resultItem: FeaturedResultsItem, idx: number) => ( 34 | 35 |

36 | 37 | { 41 | submitFeedback( 42 | Relevance['Click'], 43 | resultItem.Id ?? '', 44 | queryId 45 | ); 46 | }} 47 | isExternal> 48 | 56 | 57 | 58 | 59 | 60 |

61 | 62 | 63 | 71 | 72 | 73 |
74 | ))} 75 |
76 |
77 | ); 78 | } else { 79 | return <>; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /packages/web/src/layout/MainArea.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { Flex } from '@chakra-ui/react'; 5 | import FilterArea from './FilterArea'; 6 | import { VStack } from '@chakra-ui/layout'; 7 | import { useGlobalContext } from '../utils/useGlobalContext'; 8 | import Kendra from './KendraAreaAssets/KendraAreaMain'; 9 | import { HStack } from '@chakra-ui/react'; 10 | // i18 11 | import AiArea from './AiArea'; 12 | 13 | const MainArea = () => { 14 | const { currentConversation: currentConversation } = useGlobalContext(); 15 | 16 | return ( 17 | 23 | {/* フィルター */} 24 | 25 | 26 | {/* 本体 */} 27 | 28 | 29 | {(() => { 30 | if (currentConversation !== undefined) { 31 | return ; 32 | } 33 | return <>; 34 | })()} 35 | 36 | 37 | 38 | {/* AI */} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default MainArea; 45 | -------------------------------------------------------------------------------- /packages/web/src/layout/TOTP.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { AmplifyUser } from '@aws-amplify/ui'; 5 | import { Auth } from 'aws-amplify'; 6 | import QRCode from 'qrcode'; 7 | import React, { useState } from 'react'; 8 | import { 9 | Alert, 10 | AlertIcon, 11 | Button, 12 | MenuItem, 13 | Input, 14 | Text, 15 | Modal, 16 | ModalOverlay, 17 | ModalContent, 18 | ModalHeader, 19 | ModalBody, 20 | useDisclosure, 21 | useToast, 22 | } from '@chakra-ui/react'; 23 | // i18 24 | import { useTranslation } from 'react-i18next'; 25 | 26 | const samlAuthEnabled: boolean = 27 | import.meta.env.VITE_APP_SAMLAUTH_ENABLED === 'true'; 28 | 29 | interface CustomSetupTOTPProps { 30 | user: AmplifyUser | undefined; 31 | issuer: string; 32 | handleAuthStateChange: () => void; 33 | } 34 | 35 | export function CustomSetupTOTP(props: CustomSetupTOTPProps) { 36 | // MFA 機能 37 | 38 | const [mfaEnabled, setMfaEnabled] = useState(false); 39 | const [isLoading, setIsLoading] = useState(true); 40 | const [isVerifyingToken, setIsVerifyingToken] = useState(false); 41 | const [qrCode, setQrCode] = React.useState(''); 42 | const [token, setToken] = React.useState(''); 43 | const [errorMessage, setErrorMessage] = React.useState(''); 44 | const { isOpen, onOpen, onClose } = useDisclosure(); 45 | const toast = useToast(); 46 | 47 | // totp(Time-based One-time Password) を生成 48 | const getTotpCode = ( 49 | issuer: string, 50 | username: string, 51 | secret: string 52 | ): string => 53 | encodeURI( 54 | `otpauth://totp/${issuer}:${username}?secret=${secret}&issuer=${issuer}` 55 | ); 56 | 57 | const totpUsername = props.user?.getUsername() || ''; 58 | 59 | // 言語設定 60 | const { t } = useTranslation(); 61 | 62 | const generateQRCode = React.useCallback( 63 | async (currentUser: AmplifyUser): Promise => { 64 | // QRコードを生成 65 | 66 | try { 67 | const newSecretKey = await Auth.setupTOTP(currentUser); 68 | const totpCode = getTotpCode(props.issuer, totpUsername, newSecretKey); 69 | const qrCodeImageSource = await QRCode.toDataURL(totpCode); 70 | setQrCode(qrCodeImageSource); 71 | } catch (error) { 72 | console.error(error); 73 | } finally { 74 | setIsLoading(false); 75 | } 76 | }, 77 | [props.issuer, totpUsername] 78 | ); 79 | 80 | const verifyTotpToken = () => { 81 | // 確認後、ユーザーは TOTP を生成するアプリ (Google 認証システムなど) に TOTP アカウントを持つ 82 | // 生成されたワンタイムパスワードを使用して設定を検証 83 | 84 | setErrorMessage(''); 85 | setIsVerifyingToken(true); 86 | Auth.verifyTotpToken(props.user, token) 87 | .then(async () => { 88 | await Auth.setPreferredMFA(props.user, 'TOTP'); 89 | props.handleAuthStateChange(); 90 | return null; 91 | }) 92 | .catch((e) => { 93 | console.error(e); 94 | if (/Code mismatch/.test(e.toString())) { 95 | setErrorMessage('セキュリティコードが違います'); 96 | } 97 | }) 98 | .finally(() => { 99 | setIsVerifyingToken(false); 100 | onClose(); 101 | toast({ 102 | title: t('toast.mfa_success'), 103 | status: 'success', 104 | duration: 9000, 105 | isClosable: true, 106 | }); 107 | }); 108 | }; 109 | 110 | React.useEffect(() => { 111 | if (!props.user) { 112 | return; 113 | } 114 | Auth.getPreferredMFA(props.user).then((data) => { 115 | if (data != 'NOMFA') { 116 | setMfaEnabled(true); 117 | } 118 | }); 119 | void generateQRCode(props.user); 120 | }, [generateQRCode, props.user, isOpen]); 121 | 122 | const isValidToken = () => { 123 | return /^\d{6}$/gm.test(token); 124 | }; 125 | 126 | if (mfaEnabled) return null; 127 | if (samlAuthEnabled) return null; 128 | 129 | return ( 130 | <> 131 | {t('top_bar.set_mfa')} 132 | 133 | 134 | 135 | {t('top_bar.set_mfa')} 136 | 137 |
{ 139 | event.preventDefault(); 140 | }}> 141 | {isLoading &&
{'loading...'}
} 142 | {!isLoading && ( 143 | <> 144 | qr code 151 | 152 | { 153 | 'QR コードを読み込み、セキュリティコードを入力してください。' 154 | } 155 | 156 | { 160 | setToken(e.target.value); 161 | }}> 162 | {errorMessage && ( 163 | { 166 | setErrorMessage(''); 167 | }}> 168 | 169 | {errorMessage} 170 | 171 | )} 172 | 183 | 184 | )} 185 |
186 |
187 |
188 |
189 | 190 | ); 191 | } 192 | -------------------------------------------------------------------------------- /packages/web/src/layout/TopBar.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { 5 | Flex, 6 | Text, 7 | useColorModeValue, 8 | Button, 9 | MenuButton, 10 | Menu, 11 | MenuList, 12 | MenuItem, 13 | HStack, 14 | } from '@chakra-ui/react'; 15 | import { ChevronDownIcon } from '@chakra-ui/icons'; 16 | import { CustomSetupTOTP } from './TOTP.tsx'; 17 | import { UseAuthenticator } from '@aws-amplify/ui-react-core'; 18 | import { AmplifyUser } from '@aws-amplify/ui'; 19 | // i18 20 | import { useTranslation } from 'react-i18next'; 21 | import InputWithSuggest from './InputWithSuggest.tsx'; 22 | 23 | export type SignOut = UseAuthenticator['signOut']; 24 | 25 | export default function TopBar({ 26 | logout, 27 | user, 28 | }: { 29 | logout: SignOut | undefined; 30 | user: AmplifyUser | undefined; 31 | }) { 32 | // 言語設定 33 | const { t } = useTranslation(); 34 | 35 | return ( 36 | 50 | {/* タイトル */} 51 | 52 | 53 | Amazon Kendra 54 | 55 | 56 | 57 | {/* 検索バー */} 58 | 59 | 60 | 61 | 62 | {/* アカウントの設定 */} 63 | 64 | 65 | }> 66 | {t('top_bar.account')} 67 | 68 | 69 | {/* MFAボタン */} 70 | null}> 74 | {/* ログアウトボタン */} 75 | {t('top_bar.logout')} 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /packages/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import ReactDOM from 'react-dom/client'; 5 | // import './index.css' 6 | import { ChakraProvider } from '@chakra-ui/react'; 7 | import { Authenticator } from '@aws-amplify/ui-react'; 8 | import AuthWithSAML from './components/AuthWithSAML.tsx'; 9 | import AuthWithUserpool from './components/AuthWithUserpool.tsx'; 10 | 11 | const samlAuthEnabled: boolean = 12 | import.meta.env.VITE_APP_SAMLAUTH_ENABLED === 'true'; 13 | 14 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 15 |
16 | 17 | 18 | {samlAuthEnabled ? : } 19 | 20 | 21 |
22 | ); 23 | -------------------------------------------------------------------------------- /packages/web/src/utils/constant.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { selectItemType } from './interface'; 5 | 6 | // language setting 7 | export const LANGUAGES: selectItemType[] = [ 8 | { name: 'English', value: 'en' }, 9 | { name: 'Spanish', value: 'es' }, 10 | { name: 'French', value: 'fr' }, 11 | { name: 'German', value: 'de' }, 12 | { name: 'Portuguese', value: 'pt' }, 13 | { name: 'Japanese', value: 'ja' }, 14 | { name: 'Korean', value: 'ko' }, 15 | { name: 'Chinese', value: 'zh' }, 16 | { name: 'Italian', value: 'it' }, 17 | { name: 'Hindi', value: 'hi' }, 18 | { name: 'Arabic', value: 'ar' }, 19 | { name: 'Armenian', value: 'hy' }, 20 | { name: 'Basque', value: 'eu' }, 21 | { name: 'Bengali', value: 'bn' }, 22 | { name: 'Brazilian', value: 'pt-BR' }, 23 | { name: 'Bulgarian', value: 'bg' }, 24 | { name: 'Catalan', value: 'ca' }, 25 | { name: 'Czech', value: 'cs' }, 26 | { name: 'Danish', value: 'da' }, 27 | { name: 'Dutch', value: 'nl' }, 28 | { name: 'Finnish', value: 'fi' }, 29 | { name: 'Galician', value: 'gl' }, 30 | { name: 'Greek', value: 'el' }, 31 | { name: 'Hungarian', value: 'hu' }, 32 | { name: 'Indonesian', value: 'id' }, 33 | { name: 'Irish', value: 'ga' }, 34 | { name: 'Latvian', value: 'lv' }, 35 | { name: 'Lithuanian', value: 'lt' }, 36 | { name: 'Norwegian', value: 'no' }, 37 | { name: 'Persian', value: 'fa' }, 38 | { name: 'Romanian', value: 'ro' }, 39 | { name: 'Russian', value: 'ru' }, 40 | { name: 'Sorani', value: 'ckb' }, 41 | { name: 'Swedish', value: 'sv' }, 42 | { name: 'Turkish', value: 'tr' }, 43 | ]; 44 | 45 | export const DEFAULT_LANGUAGE: string = 'ja'; 46 | 47 | export const DEFAULT_SORT_ATTRIBUTE = 'Relevance'; 48 | 49 | export enum SortOrderEnum { 50 | Desc = 'DESC', 51 | Asc = 'ASC', 52 | } 53 | 54 | export const SEARCH_MODE_LIST = ['#rag', '#kendra', '#ai']; 55 | export const DEFAULT_SEARCH_MODE = '#rag'; 56 | 57 | export const DEFAULT_SORT_ORDER = SortOrderEnum.Desc; 58 | 59 | export const LANGUAGE_INDEX = 0; 60 | export const SORT_ATTRIBUTE_INDEX = 0; 61 | export const SORT_ORDER_INDEX = 1; 62 | export const SORT_ORDER = ['ASC', 'DESC']; 63 | export const MIN_INDEX = 0; 64 | export const MAX_INDEX = 1; 65 | export const RECENT_QUERY_CAPACITY = 3; 66 | export const MAX_QUERY_SUGGESTION = 3; 67 | 68 | // `aws kendra get-snapshots --index-id --interval THIS_WEEK --metric-type QUERIES_BY_COUNT` の結果を模したトップクエリのモックデータを読み込む 69 | import topQueriesData from './top_queries.json'; 70 | 71 | // CTR をベースに降順でソートし クエリだけを取り出して string[] 型に変換 72 | const sortedData = topQueriesData.SnapshotsData.sort( 73 | (a: (string | number)[], b: (string | number)[]) => 74 | (b[2] as number) - (a[2] as number) 75 | ); 76 | export const TOP_QUERIES: string[] = sortedData.map( 77 | (data: (string | number)[]) => data[0].toString() 78 | ); 79 | 80 | // 生成AIで生み出すクエリ候補の数 81 | export const MAX_QUERY_SUGGESTIONS = 3; 82 | -------------------------------------------------------------------------------- /packages/web/src/utils/globalContext.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, createContext } from 'react'; 2 | import { AiAgentHistory, Conversation, Dic, Filter } from './interface.tsx'; 3 | 4 | // Global変数 5 | interface GlobalContextInterface { 6 | // 現在の結果 7 | currentConversation: Conversation | undefined; 8 | setCurrentConversation: Dispatch>; 9 | // 現在適用中のフィルタ 10 | filterOptions: Filter[]; 11 | setFilterOptions: Dispatch>; 12 | // Datasource情報 13 | datasourceInfo: Dic; 14 | setDatasourceInfo: Dispatch>; 15 | // 入力中の文字列 16 | currentInputText: string; 17 | setCurrentInputText: Dispatch>; 18 | // 直近のクエリ 19 | recentQueryList: string[]; 20 | setRecentQueryList: Dispatch>; 21 | 22 | // 検索履歴ID 23 | currentQueryId: string; 24 | setCurrentQueryId: Dispatch>; 25 | 26 | // AI Agent の利用履歴 27 | aiAgent: AiAgentHistory; 28 | setAiAgent: Dispatch>; 29 | } 30 | 31 | export const GlobalContext = createContext( 32 | undefined 33 | ); 34 | -------------------------------------------------------------------------------- /packages/web/src/utils/interface.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { QueryRequest, QueryResult } from '@aws-sdk/client-kendra'; 5 | 6 | export interface Dic { 7 | /** 辞書型 */ 8 | [key: string]: string; 9 | } 10 | 11 | export interface UserInput { 12 | /** ユーザーからの入力 */ 13 | word: string; 14 | } 15 | 16 | export interface AiResponse { 17 | /** AIからの返答 */ 18 | userUtterance: string; // ユーザーからの入力 19 | aiUtterance: string; // AIからの応答 20 | actualPrompt: string; // AIに入力されたプロンプト 21 | // memory: any; // 入力以前のやりとり 22 | usedTemplatePrompt: string; // 利用したテンプレート 23 | contexts: DocumentForInf[]; // テンプレートに埋め込む変数 24 | llmParam: Dic; // LLMの設定値 25 | } 26 | // やり取りの種類 27 | export type ConversationType = 28 | | 'HUMAN' 29 | | 'HUMAN_AI' 30 | | 'HUMAN_KENDRA' 31 | | 'HUMAN_KENDRA_AI'; 32 | 33 | // やり取り 34 | export interface Conversation { 35 | /** やり取り */ 36 | conversationType: ConversationType; // やり取りの種類 37 | userInput: UserInput; // ユーザー入力 38 | userQuery: QueryRequest | undefined; // Kendraへの入力 39 | kendraResponse: QueryResult | undefined; // Kendraからの出力 40 | aiResponse: AiResponse | undefined; 41 | } 42 | 43 | // 1選択式のフィルタアイテム 44 | export type selectItemType = { 45 | name: string; 46 | value: string; 47 | }; 48 | 49 | // フィルタの種類 50 | export type FilterType = 51 | | 'LAUNGUAGE_SETTING' 52 | | 'SORT_BY' 53 | | 'SELECT_ONE_STRING' 54 | | 'CONTAIN_STRING' 55 | | 'RANGE_NUM' 56 | | 'RANGE_DATE' 57 | | 'SELECT_MULTI_STRING_FROM_LIST'; 58 | 59 | // フィルタ 60 | export interface Filter { 61 | filterType: FilterType; 62 | title: string; 63 | options: selectItemType[]; 64 | selected: string[] | boolean[] | number[] | Date[]; 65 | } 66 | 67 | // LLM で推論するためのデータ型 68 | type DocTypeForInf = 'DOCUMENT' | 'QUESTION_ANSWER' | 'ANSWER'; 69 | export interface DocumentForInf { 70 | excerpt: string; 71 | title: string; 72 | content: string; 73 | type: DocTypeForInf; 74 | } 75 | 76 | // AIに選択された情報 77 | export interface AiSelectedInfo { 78 | title: string; 79 | chunk: string; 80 | url: string; 81 | lastUpdate: string; 82 | feadbackToken: string; 83 | } 84 | 85 | // AI Agentのステータス 86 | export interface AiAgentStatus { 87 | aiAgentResponse: string; 88 | aiSelectedInfoList: AiSelectedInfo[]; 89 | suggestedQuery: string[]; 90 | systemLog: string[]; 91 | diveDeepIsEnabled: boolean; 92 | userQuery: string; 93 | } 94 | 95 | // AI Agentの利用履歴 96 | export interface AiAgentHistory { 97 | [queryId: string]: AiAgentStatus; 98 | } 99 | 100 | export interface Quote { 101 | documentIndex: number; 102 | text: string; 103 | } 104 | 105 | export interface Answer { 106 | quotes: Quote[]; 107 | } 108 | -------------------------------------------------------------------------------- /packages/web/src/utils/service.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the MIT-0 License (https://github.com/aws/mit-0) 3 | 4 | import { 5 | AttributeFilter, 6 | DescribeIndexCommand, 7 | ListDataSourcesCommand, 8 | QueryCommand, 9 | QueryCommandInput, 10 | QueryCommandOutput, 11 | SortingConfiguration, 12 | SubmitFeedbackCommand, 13 | ListDataSourcesCommandOutput, 14 | } from '@aws-sdk/client-kendra'; 15 | import { 16 | InvokeWithResponseStreamCommand, 17 | LambdaClient, 18 | } from '@aws-sdk/client-lambda'; 19 | import { fromCognitoIdentityPool } from '@aws-sdk/credential-provider-cognito-identity'; 20 | import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'; 21 | 22 | import { Dic, Filter, selectItemType } from './interface'; 23 | import { DEFAULT_SORT_ATTRIBUTE, DEFAULT_SORT_ORDER } from './constant'; 24 | import { Amplify, Auth } from 'aws-amplify'; 25 | import axios from 'axios'; 26 | import { PredictRequest } from 'jp-rag-sample'; 27 | // import awsconfig from '../aws-exports'; 28 | 29 | const _loadingErrors = []; 30 | 31 | // if (!import.meta.env.VITE_INDEX_ID) { 32 | // _loadingErrors.push('環境変数にINDEX_IDがありません'); 33 | // } 34 | if (!import.meta.env.VITE_APP_PREDICT_STREAM_FUNCTION_ARN) { 35 | _loadingErrors.push( 36 | '環境変数に VITE_APP_PREDICT_STREAM_FUNCTION_ARN がありません' 37 | ); 38 | } 39 | if (!import.meta.env.VITE_APP_MODEL_IDS) { 40 | _loadingErrors.push('環境変数に VITE_APP_MODEL_IDS がありません'); 41 | } 42 | const hasErrors = _loadingErrors.length > 0; 43 | if (hasErrors) { 44 | console.error(JSON.stringify(_loadingErrors)); 45 | } 46 | 47 | export const initAWSError: string[] = _loadingErrors; 48 | 49 | export const indexId: string = import.meta.env.VITE_APP_KENDRA_INDEX_ID ?? ''; 50 | const stream_func_name: string = 51 | import.meta.env.VITE_APP_PREDICT_STREAM_FUNCTION_ARN ?? ''; 52 | const remote_server = import.meta.env.VITE_APP_API_ENDPOINT ?? ''; 53 | export const serverUrl: string = remote_server; 54 | const bedrockModelIds: string[] = JSON.parse(import.meta.env.VITE_APP_MODEL_IDS) 55 | .map((name: string) => name.trim()) 56 | .filter((name: string) => name); 57 | const model_id: string = bedrockModelIds[0]; 58 | // const bedrock_region: string = import.meta.env.VITE_BEDROCK_REGION ?? ''; 59 | // let jwtToken = ''; 60 | 61 | Amplify.configure({ 62 | Auth: { 63 | userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, 64 | userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, 65 | identityPoolId: import.meta.env.VITE_APP_IDENTITY_POOL_ID, 66 | authenticationFlowType: 'USER_SRP_AUTH', 67 | }, 68 | }); 69 | 70 | const api = axios.create({ 71 | baseURL: import.meta.env.VITE_APP_API_ENDPOINT, 72 | }); 73 | 74 | // // HTTP Request Preprocessing 75 | api.interceptors.request.use(async (config) => { 76 | // If Authenticated, append ID Token to Request Header 77 | const user = await Auth.currentAuthenticatedUser(); 78 | if (user) { 79 | const token = (await Auth.currentSession()).getIdToken().getJwtToken(); 80 | config.headers['Authorization'] = token; 81 | } 82 | config.headers['Content-Type'] = 'application/json'; 83 | 84 | return config; 85 | }); 86 | 87 | // export function setJwtToken(token: string) { 88 | // jwtToken = token; 89 | // } 90 | 91 | export enum Relevance { 92 | Relevant = 'RELEVANT', 93 | NotRelevant = 'NOT_RELEVANT', 94 | Click = 'CLICK', 95 | } 96 | 97 | export async function submitFeedback( 98 | relevance: Relevance, // feedbackする関連度 99 | resultId: string, // feedbackするアイテム 100 | queryId: string // Query id 101 | ) { 102 | /** 103 | * 増分学習のための Feedbackを送信 104 | */ 105 | const command = 106 | relevance === Relevance.Click 107 | ? new SubmitFeedbackCommand({ 108 | IndexId: indexId, 109 | QueryId: queryId, 110 | ClickFeedbackItems: [ 111 | { 112 | ResultId: resultId, 113 | ClickTime: new Date(), 114 | }, 115 | ], 116 | }) 117 | : new SubmitFeedbackCommand({ 118 | IndexId: indexId, 119 | QueryId: queryId, 120 | RelevanceFeedbackItems: [ 121 | { 122 | ResultId: resultId, 123 | RelevanceValue: relevance, 124 | }, 125 | ], 126 | }); 127 | 128 | // Feedbackを送信 129 | const data = await api 130 | .post('kendra/send', command) 131 | .then((response) => response.data); 132 | return data; 133 | } 134 | 135 | export async function getKendraQuery( 136 | /** 137 | * Kendra Query API への request Bodyを作成 138 | */ 139 | queryText: string, 140 | attributeFilter: AttributeFilter, 141 | sortingConfiguration: SortingConfiguration | undefined 142 | ): Promise { 143 | return { 144 | IndexId: indexId, 145 | PageNumber: 1, 146 | PageSize: 10, 147 | QueryText: queryText, 148 | AttributeFilter: attributeFilter, 149 | SortingConfiguration: sortingConfiguration, 150 | UserContext: { 151 | Token: (await Auth.currentAuthenticatedUser()).signInUserSession 152 | .accessToken.jwtToken, 153 | }, 154 | }; 155 | } 156 | 157 | export async function overwriteQuery( 158 | /** 159 | * Kendra Query API への request Bodyへフィルタリング情報を付与 160 | */ 161 | prevQuery: QueryCommandInput, 162 | newAttributeFilter: AttributeFilter, 163 | newSortingConfiguration: SortingConfiguration | undefined 164 | ): Promise { 165 | return { 166 | ...prevQuery, 167 | AttributeFilter: newAttributeFilter, 168 | SortingConfiguration: newSortingConfiguration, 169 | UserContext: { 170 | Token: (await Auth.currentAuthenticatedUser()).signInUserSession 171 | .accessToken.jwtToken, 172 | }, 173 | }; 174 | } 175 | 176 | export async function kendraQuery(param: QueryCommandInput) { 177 | /** 178 | * Kendra Query API を実行 179 | */ 180 | 181 | const data = await api 182 | .post('kendra/query', new QueryCommand(param)) 183 | .then((response) => response.data) 184 | .then((r: QueryCommandOutput) => { 185 | return r; 186 | }); 187 | 188 | return data; 189 | } 190 | 191 | export async function getDatasourceInfo(): Promise { 192 | /* 193 | * DataSource の情報を取得 194 | */ 195 | const command = new ListDataSourcesCommand({ 196 | IndexId: indexId, 197 | }); 198 | 199 | const data = await api 200 | .post('kendra/listDataSources', command) 201 | .then((response) => response.data) 202 | .then((r: ListDataSourcesCommandOutput) => { 203 | // Datasource list を {id:name} の dict に変換 204 | const datasourceDic: Dic = {}; 205 | for (const datasourceItem of r.SummaryItems ?? []) { 206 | datasourceDic[datasourceItem.Id ?? ''] = datasourceItem.Name ?? ''; 207 | } 208 | return datasourceDic; 209 | }); 210 | return data; 211 | } 212 | 213 | export async function getSortOrderFromIndex(): Promise { 214 | /* 215 | * Index から並び順の候補を取得 216 | */ 217 | const sortingAttributeDateList: selectItemType[] = [ 218 | { name: DEFAULT_SORT_ATTRIBUTE, value: DEFAULT_SORT_ATTRIBUTE }, 219 | ]; 220 | 221 | // indexidを使いkendraから情報を取得 222 | const command = new DescribeIndexCommand({ 223 | Id: indexId, 224 | }); 225 | 226 | await api 227 | .post('kendra/describeIndex', command) 228 | .then((response) => response.data) 229 | .then((v) => { 230 | const configList = v.DocumentMetadataConfigurations; 231 | // sortableなファセットの候補を取得 232 | if (configList) { 233 | for (const documentMetadataConfig of configList) { 234 | if ( 235 | documentMetadataConfig && 236 | documentMetadataConfig.Search?.Sortable && 237 | documentMetadataConfig.Name 238 | ) { 239 | sortingAttributeDateList.push({ 240 | name: documentMetadataConfig.Name, 241 | value: documentMetadataConfig.Name, 242 | }); 243 | } 244 | } 245 | } 246 | }); 247 | 248 | return { 249 | filterType: 'SORT_BY', 250 | title: '並び順', 251 | options: sortingAttributeDateList, 252 | selected: [DEFAULT_SORT_ATTRIBUTE, DEFAULT_SORT_ORDER], 253 | }; 254 | } 255 | 256 | export async function* infStreamClaude(user_prompt: string) { 257 | /** 258 | * Claude で ストリーミング推論 259 | */ 260 | // 認証情報 261 | const region = import.meta.env.VITE_APP_REGION; 262 | const userPoolId = import.meta.env.VITE_APP_USER_POOL_ID; 263 | const idPoolId = import.meta.env.VITE_APP_IDENTITY_POOL_ID; 264 | const cognito = new CognitoIdentityClient({ region }); 265 | const providerName = `cognito-idp.${region}.amazonaws.com/${userPoolId}`; 266 | const lambda_client = new LambdaClient({ 267 | region: region, 268 | credentials: fromCognitoIdentityPool({ 269 | client: cognito, 270 | identityPoolId: idPoolId, 271 | logins: { 272 | [providerName]: (await Auth.currentSession()) 273 | .getIdToken() 274 | .getJwtToken(), 275 | }, 276 | }), 277 | }); 278 | 279 | const req: PredictRequest = { 280 | model: { 281 | type: 'bedrock', 282 | modelId: model_id, 283 | }, 284 | messages: [ 285 | { 286 | role: 'user', 287 | content: user_prompt, 288 | }, 289 | ], 290 | id: '', 291 | }; 292 | 293 | // ストリーミング推論 294 | const lambda_command = new InvokeWithResponseStreamCommand({ 295 | FunctionName: stream_func_name, 296 | Payload: JSON.stringify(req), 297 | }); 298 | const res = await lambda_client.send(lambda_command); 299 | 300 | // チャンクを表示 301 | const events = res.EventStream; 302 | if (!events) { 303 | return; 304 | } 305 | for await (const event of events) { 306 | if (event.PayloadChunk) { 307 | const result = new TextDecoder('utf-8').decode( 308 | event.PayloadChunk.Payload 309 | ); 310 | yield result; 311 | } 312 | 313 | if (event.InvokeComplete) { 314 | break; 315 | } 316 | } 317 | } 318 | 319 | export async function infClaude(user_prompt: string): Promise { 320 | let result = ''; 321 | for await (const chunk of infStreamClaude(user_prompt)) { 322 | result += chunk; 323 | } 324 | return result; 325 | } 326 | -------------------------------------------------------------------------------- /packages/web/src/utils/top_queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "SnapShotTimeFilter": { 3 | "StartTime": "2023-05-15T00:00:00Z", 4 | "EndTime": "2023-05-21T23:59:59Z" 5 | }, 6 | "SnapshotsDataHeader": [ 7 | "query_content", 8 | "count", 9 | "ctr", 10 | "zero_click_rate", 11 | "click_depth", 12 | "instant_answer", 13 | "confidence" 14 | ], 15 | "SnapshotsData": [ 16 | [ 17 | "claude", 18 | 25, 19 | 0.9, 20 | 0.64, 21 | 1.8, 22 | 0.04, 23 | "HIGH" 24 | ], 25 | [ 26 | "claude ai", 27 | 18, 28 | 0.22, 29 | 0.39, 30 | 2.1, 31 | 0.11, 32 | "MEDIUM" 33 | ], 34 | [ 35 | "claude 3 opus", 36 | 12, 37 | 0.17, 38 | 0.58, 39 | 1.5, 40 | 0.0, 41 | "LOW" 42 | ], 43 | [ 44 | "claude opus", 45 | 8, 46 | 0.25, 47 | 0.5, 48 | 2.3, 49 | 0.13, 50 | "HIGH" 51 | ], 52 | [ 53 | "perplexity", 54 | 38, 55 | 0.9, 56 | 0.55, 57 | 2.2, 58 | 0.08, 59 | "MEDIUM" 60 | ], 61 | [ 62 | "perplexity ai", 63 | 22, 64 | 0.27, 65 | 0.41, 66 | 2.6, 67 | 0.14, 68 | "HIGH" 69 | ], 70 | [ 71 | "perplexity とは", 72 | 15, 73 | 0.13, 74 | 0.67, 75 | 1.7, 76 | 0.20, 77 | "LOW" 78 | ], 79 | [ 80 | "perplexity pro", 81 | 10, 82 | 0.30, 83 | 0.40, 84 | 2.8, 85 | 0.10, 86 | "HIGH" 87 | ], 88 | [ 89 | "perplexity gpt 4o", 90 | 6, 91 | 0.17, 92 | 0.67, 93 | 1.8, 94 | 0.0, 95 | "LOW" 96 | ], 97 | [ 98 | "perplexity 日本語", 99 | 4, 100 | 0.25, 101 | 0.50, 102 | 2.5, 103 | 0.25, 104 | "MEDIUM" 105 | ], 106 | [ 107 | "perplexity api", 108 | 8, 109 | 0.38, 110 | 0.25, 111 | 3.1, 112 | 0.13, 113 | "HIGH" 114 | ], 115 | [ 116 | "kendra get-snapshots", 117 | 65, 118 | 0.22, 119 | 0.49, 120 | 2.7, 121 | 0.12, 122 | "HIGH" 123 | ], 124 | [ 125 | "kendra とは", 126 | 42, 127 | 0.8, 128 | 0.62, 129 | 1.9, 130 | 0.24, 131 | "MEDIUM" 132 | ], 133 | [ 134 | "kendra", 135 | 125, 136 | 0.8, 137 | 0.58, 138 | 2.1, 139 | 0.06, 140 | "LOW" 141 | ], 142 | [ 143 | "kendra 料金", 144 | 32, 145 | 0.28, 146 | 0.41, 147 | 2.9, 148 | 0.09, 149 | "HIGH" 150 | ], 151 | [ 152 | "kendra connector", 153 | 18, 154 | 0.33, 155 | 0.39, 156 | 3.2, 157 | 0.11, 158 | "HIGH" 159 | ], 160 | [ 161 | "kendra quota", 162 | 12, 163 | 0.25, 164 | 0.50, 165 | 2.6, 166 | 0.08, 167 | "MEDIUM" 168 | ], 169 | [ 170 | "kendra rag", 171 | 8, 172 | 0.13, 173 | 0.75, 174 | 1.4, 175 | 0.0, 176 | "LOW" 177 | ], 178 | [ 179 | "kendra api", 180 | 22, 181 | 0.32, 182 | 0.36, 183 | 3.0, 184 | 0.18, 185 | "HIGH" 186 | ], 187 | [ 188 | "bedrockawsconnectionproperties", 189 | 8, 190 | 0.25, 191 | 0.63, 192 | 2.1, 193 | 0.13, 194 | "MEDIUM" 195 | ], 196 | [ 197 | "bedrock 料金", 198 | 25, 199 | 0.20, 200 | 0.56, 201 | 2.4, 202 | 0.08, 203 | "LOW" 204 | ], 205 | [ 206 | "bedrock pricing", 207 | 18, 208 | 0.28, 209 | 0.44, 210 | 2.8, 211 | 0.11, 212 | "HIGH" 213 | ], 214 | [ 215 | "bedrock", 216 | 62, 217 | 0.15, 218 | 0.65, 219 | 1.9, 220 | 0.06, 221 | "LOW" 222 | ], 223 | [ 224 | "bedrock kendra", 225 | 14, 226 | 0.29, 227 | 0.43, 228 | 2.9, 229 | 0.14, 230 | "HIGH" 231 | ], 232 | [ 233 | "bedrock aws", 234 | 32, 235 | 0.22, 236 | 0.53, 237 | 2.5, 238 | 0.09, 239 | "MEDIUM" 240 | ], 241 | [ 242 | "bedrock knowledge base", 243 | 10, 244 | 0.30, 245 | 0.40, 246 | 3.1, 247 | 0.20, 248 | "HIGH" 249 | ], 250 | [ 251 | "bedrock kendra rag", 252 | 6, 253 | 0.17, 254 | 0.67, 255 | 1.8, 256 | 0.0, 257 | "LOW" 258 | ], 259 | [ 260 | "bedrock claude", 261 | 4, 262 | 0.25, 263 | 0.50, 264 | 2.8, 265 | 0.25, 266 | "MEDIUM" 267 | ] 268 | ] 269 | } -------------------------------------------------------------------------------- /packages/web/src/utils/useGlobalContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { GlobalContext } from './globalContext'; 3 | 4 | export const useGlobalContext = () => { 5 | const context = useContext(GlobalContext); 6 | 7 | if (context === undefined) { 8 | throw new Error('useCount must be used within a CountProvider'); 9 | } 10 | return context; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_API_ENDPOINT: string; 5 | readonly VITE_APP_KENDRA_INDEX_ID: string; 6 | readonly VITE_APP_REGION: string; 7 | readonly VITE_APP_USER_POOL_ID: string; 8 | readonly VITE_APP_USER_POOL_CLIENT_ID: string; 9 | readonly VITE_APP_IDENTITY_POOL_ID: string; 10 | readonly VITE_APP_PREDICT_STREAM_FUNCTION_ARN: string; 11 | readonly VITE_APP_SELF_SIGN_UP_ENABLED: string; 12 | readonly VITE_APP_VERSION: string; 13 | readonly VITE_APP_MODEL_REGION: string; 14 | readonly VITE_APP_MODEL_IDS: string; 15 | readonly VITE_APP_SAMLAUTH_ENABLED: string; 16 | readonly VITE_APP_SAML_COGNITO_DOMAIN_NAME: string; 17 | readonly VITE_APP_SAML_COGNITO_FEDERATED_IDENTITY_PROVIDER_NAME: string; 18 | } 19 | 20 | interface ImportMeta { 21 | readonly env: ImportMetaEnv; 22 | } 23 | -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "types": ["vite-plugin-svgr/client"] 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } -------------------------------------------------------------------------------- /packages/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import svgr from 'vite-plugin-svgr'; 4 | import { nodePolyfills } from 'vite-plugin-node-polyfills'; 5 | import { VitePWA } from 'vite-plugin-pwa'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(({ command }) => { 9 | return { 10 | plugins: [ 11 | react(), 12 | svgr(), 13 | nodePolyfills({ 14 | globals: { 15 | Buffer: true, 16 | process: true, 17 | }, 18 | }), 19 | VitePWA({ 20 | registerType: 'autoUpdate', 21 | devOptions: { 22 | enabled: true, 23 | }, 24 | injectRegister: 'auto', 25 | manifest: { 26 | name: 'JP RAG Sample', 27 | short_name: 'JP RAG Sample', 28 | description: 29 | 'Retrieval Augmented Generation (RAG) のアプリケーション実装', 30 | start_url: '/', 31 | display: 'minimal-ui', 32 | theme_color: '#232F3E', 33 | background_color: '#FFFFFF', 34 | icons: [ 35 | { 36 | src: '/images/aws_icon_192.png', 37 | sizes: '192x192', 38 | type: 'image/png', 39 | }, 40 | { 41 | src: '/images/aws_icon_192_maskable.png', 42 | sizes: '192x192', 43 | type: 'image/png', 44 | purpose: 'maskable', 45 | }, 46 | { 47 | src: '/images/aws_icon_512.png', 48 | sizes: '512x512', 49 | type: 'image/png', 50 | }, 51 | { 52 | src: '/images/aws_icon_512_maskable.png', 53 | sizes: '512x512', 54 | type: 'image/png', 55 | purpose: 'maskable', 56 | }, 57 | ], 58 | }, 59 | }), 60 | ], 61 | build: { 62 | rollupOptions: { 63 | output: { 64 | manualChunks: { 65 | aws: ['@aws-sdk/client-kendra'], 66 | 'chakra-ui': ['@chakra-ui/icons'], 67 | '@chakra-ui/react': ['@chakra-ui/react'], 68 | }, 69 | }, 70 | }, 71 | }, 72 | esbuild: { 73 | drop: command === 'build' ? ['console', 'debugger'] : [], 74 | }, 75 | base: './', 76 | resolve: { 77 | alias: [ 78 | { 79 | find: './runtimeConfig', 80 | replacement: './runtimeConfig.browser', // ensures browser compatible version of AWS JS SDK is used 81 | }, 82 | ], 83 | }, 84 | }; 85 | }); 86 | -------------------------------------------------------------------------------- /setup-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | STACK_NAME='JpRagSampleStack' 6 | 7 | function extract_value { 8 | echo $1 | jq -r ".Stacks[0].Outputs[] | select(.OutputKey==\"$2\") | .OutputValue" 9 | } 10 | 11 | stack_output=`aws cloudformation describe-stacks --stack-name $STACK_NAME --output json` 12 | 13 | export VITE_APP_API_ENDPOINT=`extract_value "$stack_output" 'ApiEndpoint'` 14 | export VITE_APP_KENDRA_INDEX_ID=`extract_value "$stack_output" 'KendraIndexId'` 15 | export VITE_APP_REGION=`extract_value "$stack_output" 'Region'` 16 | export VITE_APP_USER_POOL_ID=`extract_value "$stack_output" 'UserPoolId'` 17 | export VITE_APP_USER_POOL_CLIENT_ID=`extract_value "$stack_output" 'UserPoolClientId'` 18 | export VITE_APP_IDENTITY_POOL_ID=`extract_value "$stack_output" 'IdPoolId'` 19 | export VITE_APP_PREDICT_STREAM_FUNCTION_ARN=`extract_value "$stack_output" PredictStreamFunctionArn` 20 | export VITE_APP_SELF_SIGN_UP_ENABLED=`extract_value "$stack_output" SelfSignUpEnabled` 21 | export VITE_APP_MODEL_REGION=`extract_value "$stack_output" ModelRegion` 22 | export VITE_APP_MODEL_IDS=`extract_value "$stack_output" ModelIds` 23 | export VITE_APP_SAMLAUTH_ENABLED=`extract_value "$stack_output" SamlAuthEnabled` 24 | export VITE_APP_SAML_COGNITO_DOMAIN_NAME=`extract_value "$stack_output" SamlCognitoDomainName` 25 | export VITE_APP_SAML_COGNITO_FEDERATED_IDENTITY_PROVIDER_NAME=`extract_value "$stack_output" SamlCognitoFederatedIdentityProviderName` 26 | --------------------------------------------------------------------------------