├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── CICD_SETUP.md └── DEVELOPMENT.md ├── imgs ├── README.md ├── arch.drawio ├── arch.png ├── playground.gif ├── sc_lp.png ├── usecase_chat.gif ├── usecase_editorial.gif ├── usecase_generate_text.gif ├── usecase_rag.gif ├── usecase_summarize.gif └── usecase_translate.gif ├── oidc-setup.yaml ├── package-lock.json ├── package.json ├── package_models.sh ├── packages ├── cdk │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmignore │ ├── bin │ │ └── generative-ai-use-cases.ts │ ├── cdk.json │ ├── jest.config.js │ ├── lambda │ │ ├── checkEndpoint.ts │ │ ├── createChat.ts │ │ ├── createEndpoint.ts │ │ ├── createMessages.ts │ │ ├── deleteChat.ts │ │ ├── deleteEndpoint.ts │ │ ├── findChatById.ts │ │ ├── listChats.ts │ │ ├── listMessages.ts │ │ ├── predict.ts │ │ ├── predictStream.ts │ │ ├── predictTitle.ts │ │ ├── queryKendra.ts │ │ ├── repository.ts │ │ ├── retrieveKendra.ts │ │ ├── updateFeedback.ts │ │ ├── updateTitle.ts │ │ └── utils │ │ │ ├── bedrockApi.ts │ │ │ ├── prompter.ts │ │ │ └── sagemakerApi.ts │ ├── lib │ │ ├── construct │ │ │ ├── api.ts │ │ │ ├── auth.ts │ │ │ ├── database.ts │ │ │ ├── index.ts │ │ │ ├── llm.ts │ │ │ ├── rag.ts │ │ │ └── web.ts │ │ └── generative-ai-use-cases-stack.ts │ ├── models │ │ ├── llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0.tar.gz │ │ ├── llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0 │ │ │ └── serving.properties │ │ ├── llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0.tar.gz │ │ └── llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0 │ │ │ └── serving.properties │ ├── package.json │ ├── test │ │ ├── __snapshots__ │ │ │ └── generative-ai-use-cases.test.ts.snap │ │ └── generative-ai-use-cases.test.ts │ └── tsconfig.json ├── types │ ├── package.json │ └── src │ │ ├── base.d.ts │ │ ├── chat.d.ts │ │ ├── index.d.ts │ │ ├── message.d.ts │ │ ├── prompt.d.ts │ │ └── protocol.d.ts └── web │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── aws.svg │ ├── src │ ├── @types │ │ └── common.d.ts │ ├── App.tsx │ ├── assets │ │ ├── aws.svg │ │ └── model.svg │ ├── components │ │ ├── Alert.tsx │ │ ├── Button.tsx │ │ ├── ButtonCopy.tsx │ │ ├── ButtonFeedback.tsx │ │ ├── ButtonGroup.tsx │ │ ├── ButtonIcon.tsx │ │ ├── ButtonSend.tsx │ │ ├── Card.tsx │ │ ├── CardDemo.tsx │ │ ├── ChatList.tsx │ │ ├── ChatListItem.tsx │ │ ├── ChatMessage.tsx │ │ ├── Checkbox.tsx │ │ ├── DialogConfirmDeleteChat.tsx │ │ ├── Drawer.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── ExpandedField.tsx │ │ ├── HighlightText.tsx │ │ ├── InputChatContent.tsx │ │ ├── Markdown.tsx │ │ ├── MenuDropdown.tsx │ │ ├── MenuItem.tsx │ │ ├── ModalDialog.tsx │ │ ├── RowItem.tsx │ │ ├── TextEditor.tsx │ │ ├── Textarea.tsx │ │ └── Tooltip.tsx │ ├── hooks │ │ ├── useChat.ts │ │ ├── useChatApi.ts │ │ ├── useConversation.ts │ │ ├── useDrawer.ts │ │ ├── useEndpoint.ts │ │ ├── useHttp.ts │ │ ├── useModel.ts │ │ ├── useRag.ts │ │ ├── useRagApi.ts │ │ ├── useScroll.ts │ │ └── useSearch.ts │ ├── index.css │ ├── main.tsx │ ├── pages │ │ ├── ChatPage.tsx │ │ ├── ChatPlayground.tsx │ │ ├── EditorialPage.tsx │ │ ├── GenerateTextPage.tsx │ │ ├── KendraSearchPage.tsx │ │ ├── LandingPage.tsx │ │ ├── NotFound.tsx │ │ ├── RagPage.tsx │ │ ├── SummarizePage.tsx │ │ ├── TextPlayground.tsx │ │ └── TranslatePage.tsx │ ├── prompt-templates │ │ ├── bilingualRinna.json │ │ ├── claude.json │ │ ├── llama2.json │ │ ├── llmJp.json │ │ ├── prompt_template.ts │ │ └── rinna.json │ ├── prompts │ │ └── index.ts │ ├── utils │ │ └── ChatUtils.ts │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── setup-env.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | env: 7 | AWS_REGION: us-west-2 8 | 9 | jobs: 10 | deploy: 11 | permissions: 12 | 13 | id-token: write 14 | contents: read 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Get temporary credentials with OIDC 21 | uses: aws-actions/configure-aws-credentials@v3 22 | with: 23 | role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }} 24 | aws-region: ${{env.AWS_REGION}} 25 | 26 | - name: Cache Dependency 27 | uses: actions/cache@v3 28 | id: cache_dependency_id 29 | env: 30 | cache-name: cache-cdk-dependency 31 | with: 32 | path: node_modules 33 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }} 34 | restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}- 35 | 36 | - name: Deploy 37 | run: | 38 | npm ci 39 | npm run cdk:deploy -- --require-approval="never" --all 40 | 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | pull_request: 5 | branches: ['main'] 6 | 7 | jobs: 8 | check-lint-build: 9 | name: 'Check lint' 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [18.x] 15 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | cache: 'npm' 24 | - run: npm ci 25 | - run: npm run lint -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | プレイグラウンドの開発への協力に関心を持っていただきありがとうございます。バグの報告、新機能の提案、それらの修正 / 実装、またドキュメントの充実にはコミュニティからの貢献が大きな力となります。 4 | 5 | Issue や Pull Request を送信する前にこのドキュメントをよく読み、必要な情報がすべて揃っていることを確認してください。 6 | 7 | ## Issue でバグの報告や新機能の提案を行う 8 | 9 | GitHub Issue を使用してバグを報告したり、新機能を提案したりすることを歓迎します。 Issue を起票する前に既存または解決済みの Issue を探すことで解決策が得られることがあるので、事前に確認をしてください。もしこれまで議論されておらず解決もされていない場合、 Issue Template に従い起票をしてください。できるだけ多くの情報を含めるようにしてください。次のような詳細は非常に役立ちます。 10 | 11 | * 再現可能なテストケースまたは一連のステップ 12 | * 使用しているコードのバージョン 13 | * バグに関連して行った変更 14 | * 環境または展開に関する異常な点 15 | 16 | ## Pull Request で貢献する 17 | 18 | Pull Request による貢献は大歓迎です。 Pull Request を送信する前に、次のことを確認してください。 19 | 20 | 1. *main* ブランチ上の最新のソースに対して作業しています。 21 | 2. 既存の Pull Request をチェックして、他の人がまだ問題に対処していないことを確認します。 22 | 3. 重要な作業について話し合うために Issue を開きます。あなたの時間を無駄にすることは望ましくありません。 23 | 24 | 次のボードを参照することで、バグ・追加要望の対応状況を参照することができます。 25 | 26 | [llmjp playground development board](https://github.com/orgs/llm-jp/projects/3) 27 | 28 | Pull Request を送信するには、次の手順を実行してください。開発環境の構築は [DEVELOPMENT.md](docs/DEVELOPMENT.md) を参照してください。 29 | 30 | 1. 本リポジトリを Fork します (Commit 権限がある場合 Clone で構いません) 。 31 | * 参考 : [forking a repository](https://help.github.com/articles/fork-a-repo/) 32 | 2. ソースを変更します。あなたが提案する修正に集中し変更してください。すべてのコードを再フォーマットしたりすると、変更に焦点を当てることが難しくなります。 33 | 3. ローカルでのテストにパスすることを確認します。 34 | * ソースコードの成形 : `npm run lint` 35 | 4. 変更内容が明確なコミットメッセージでコミットし、 Fork したリポジトリに push します。 36 | 5. Fork したリポジトリの Pull Request の画面から、本リポジトリの `main` ブランチに対し Pull Request を作成します。 37 | * [**DynamoDB のスキーマ構造を変える時はバックアップを取得してください**](https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/BackupRestore.html) 38 | 6. Pull Request に対しレビューを受けます。レビューが完了したらマージを行います。 39 | * CI/CD の仕組みに関心ある場合は [CICD Setup](./docs/CICD_SETUP.md) をご参照ください。 40 | 41 | GitHub のガイドも参考にしてください [pull request の作成](https://docs.github.com/ja/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) 42 | 43 | ## 貢献できる Issue を見つける 44 | 45 | [`good first issue` がついた Issue ](https://github.com/llm-jp/llm-jp-model-playground/labels/good%20first%20issue) は最初の貢献に適しています。自身に AWS やアプリケーション開発の知見がある場合、 `help wanted` がついた Issue をぜひサポートしてください。 46 | 47 | ## 行動規範 48 | 49 | 本プロジェクトの行動規範は、 [CONTRIBUTOR COVENANT CODE OF CONDUCT 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) に基づきます。行動規範への違反を見つけた場合や規範に関する質問は `llm-jp@nii.ac.jp` までご連絡ください。 50 | 51 | ## セキュリティの報告について 52 | 53 | 本プロジェクトについてセキュリティの脆弱性などを発見した場合は、 `llm-jp@nii.ac.jp` までご連絡ください。**決して Public な Issue で報告しないでください** 54 | 55 | ## ライセンス 56 | 57 | 本プロジェクトのライセンスは [LICENSE](LICENSE) を参照してください。本プロジェクトへの貢献を行う際は、このライセンスの範囲内で利用可能なものであるかご確認をお願いいたします。例えば、本プロジェクトのライセンスよりも Limited なライセンスのソフトウェアやコードを含めることはできません。 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LLM JP Model Playground 2 | 3 | 本リポジトリでは、大規模言語モデルのプレイグラウンドを AWS で実装するためのコードを提供します。プレイグラウンドでは、プロンプトやパラメーターを変更した際のモデルの挙動を確認、記録することができます。 4 | 5 | ![sc_lp.png](/imgs/sc_lp.png) 6 | 7 | ## 機能一覧 8 | 9 | > :white_check_mark: ... 実装されている、:construction: ... まだ実装されていない 10 | 11 | - :white_check_mark: プロンプト / パラメーター入力、出力表示用 UI 12 | - :construction: Fine-tuning 用のデータ収集 13 | - :construction: Fine-tuning 用データのラベリング 14 | - :construction: Fine-tuning の実行 15 | 16 | ## アーキテクチャ 17 | 18 | プレイグラウンドのフロントエンドは [React](https://ja.react.dev/) で実装されています。静的ファイルは Amazon S3 に配置され、 Amazon CloudFront で配信されます。バックエンドの API は Amazon API Gateway + AWS Lambda で実装され、認証は Amazon Congito で行っています。チャット履歴等の保存には Amazon DynamoDB を使用しています。大規模言語モデルは Amazon SageMaker でホスティングしています。 19 | 20 | ![arch.png](/imgs/arch.png) 21 | 22 | ## デプロイ 23 | 24 | 本リポジトリのアプリケーションをデプロイするのに [AWS Cloud Development Kit](https://aws.amazon.com/jp/cdk/)(以降 CDK)が必要です。各環境での CDK のインストール方法は [AWS CDK Workshop](https://cdkworkshop.com/ja/15-prerequisites/100-awscli.html) の「必要条件」を参照ください。本リポジトリでは TypeScript を使っているため、 Python/.NET/Java/Go の環境構築は必要ありません。 25 | 26 | リモートの環境を使用することで、お手元の PC に影響を与えずデプロイすることができます。 AWS のリモート開発環境である Cloud9 を使用したデプロイ方法については[動画](https://youtu.be/9sMA17OKP1k?si=XwEp7q6b_EXDBP3p)にまとめていますので、そちらを参考にデプロイしてください。 27 | 28 | CDK のインストールができたらデプロイを開始します。はじめに、デプロイする AWS のリージョンを設定します。 GPU インスタンス (`g5.2xlarge` など ) に余裕がある `us-west-2` を推奨します。 29 | 30 | Mac/Linux 31 | 32 | ```bash 33 | export AWS_DEFAULT_REGION=us-west-2 34 | ``` 35 | 36 | Windows 37 | ```bash 38 | set AWS_DEFAULT_REGION=us-west-2 39 | ``` 40 | 41 | 本アプリケーションに必要な `npm` のパッケージをインストールします。 42 | 43 | ```bash 44 | npm ci 45 | ``` 46 | 47 | CDK を利用したことがない場合、初回のみ CDK 用のファイルを保存する Amazon S3 やデプロイに必要なアクセス権限を付与する IAM ロールを準備する [Bootstrap](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping.html) 作業が必要です。すでに Bootstrap された環境では以下のコマンドは不要です。 48 | 49 | ```bash 50 | npx -w packages/cdk cdk bootstrap 51 | ``` 52 | 53 | 続いて、以下のコマンドで AWS リソースをデプロイします。デプロイが完了するまで、お待ちください(20 分程度かかる場合があります)。 54 | 55 | ```bash 56 | npm run cdk:deploy 57 | ``` 58 | 59 | > [!NOTE] 60 | > 開発環境を構築する際は、本番環境を上書きしないよう `-c` で `stage` を使用してください。 61 | > ```bash 62 | > npm run cdk:deploy -- -c stage= 63 | > ``` 64 | 65 | アプリケーションのデプロイが完了したら、コンソールに出力される `WebUrl` の URL からアクセスしてください。コンソールの出力をとり漏らした場合は、 AWS Console にログインし次の手順で確認してください。 66 | 67 | 1. 画面上部の検索バーで "CloudFormation" を検索し、 CloudFormation の管理画面に遷移。 68 | * この時、 AWS のリージョンが CDK deploy したリージョンで同じであることを確認してください。 69 | 2. 左側のメニューから「スタック」を選択。メインパネルに表示されたスタックの一覧から "GenerativeAiUseCasesStack" を選択。 70 | 3. 画面右にスタックの情報が出力される。「出力」のタブからキーが "WebUrl" になっているものを探すと、値の箇所に URL が記載されている。 71 | 72 | URL にアクセスするとログイン画面に遷移します。アカウントを作成する際は、 AWS Console にアクセスし次の手順で作成してください。 73 | 74 | ### アカウント作成手順 75 | 76 | 1. 画面上部の検索バーで "Cognito" を検索し、 Amazon Cognito の管理画面に遷移。 77 | * この時、 AWS のリージョンが CDK deploy したリージョンで同じであることを確認してください。 78 | 2. ユーザープールを選択 79 | * ユーザープールが複数ある場合は、上述の `WebUrl` の特定方法をなぞりキーが `UserPoolId` の値を確認し、ユーザープール ID が確認した値と一致するものを選択してください。 80 | 3. 「ユーザー」のタブから「ユーザーを作成」を押す。 81 | 4. E メールアドレス、パスワードを入力し「ユーザーを作成」を押す。 82 | * パスワードを E メールで連絡したい場合は、「 E メールで招待を送信」を選択してください。それ以外の場合、 Slack の DM など個別の方法で送ってください。 83 | 84 | ### プレイグラウンドの利用方法 85 | 86 | 下記動画を参考にしてください。エンドポイントが起動していない場合は差最初に起動する必要があります。起動したエンドポイントは、リクエストがない場合自動的に停止します。 87 | 88 | ![playground.gif](./imgs/playground.gif) 89 | 90 | ## 開発 91 | 92 | 本アプリケーションを修正する場合、 [DEVELOPMENT.md](docs/DEVELOPMENT.md) を参照し開発環境を構築してください。修正内容を本リポジトリに送っていただける場合は、 [CONTRIBUTING](CONTRIBUTING.md) を参照ください。 93 | 94 | 次のボードを参照することで、バグ・追加要望の対応状況を参照することができます。 95 | 96 | [llmjp playground development board](https://github.com/orgs/llm-jp/projects/3) 97 | 98 | ### Release History 99 | 100 | 太字は終了した Release です。 101 | 102 | * 0.5.0 : プレイグラウンドから Fine Tuning したモデルに対しプロンプト / パラメーターの入出力ができるようにする 103 | * 0.4.0 : プレイグラウンドから Fine Tuning を実行できるようにする 104 | * 0.3.0 : プレイグラウンド上でデータの参照・修正機能を実装する 105 | * 0.2.0 : プレイグラウンド上でのデータ収集機能を実装する 106 | * [0.1.1](https://github.com/llm-jp/llm-jp-model-playground/milestone/2) : 新規開発メンバーが過大な請求や障害を起こすことなく開発できるガイド、仕組みを整える。 107 | * [0.1.0](https://github.com/llm-jp/llm-jp-model-playground/milestone/1) : プロンプト / パラメーター入力、出力表示用 UI を実装する。 108 | 109 | ## License 110 | 111 | This library is licensed under the MIT-0 License. See the LICENSE file. 112 | -------------------------------------------------------------------------------- /docs/CICD_SETUP.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions を用いたCI/CDの設定方法 2 | 3 | ## 前提条件 4 | - [README.md/デプロイ](../README.md#デプロイ) 内の `npx -w packages/cdk cdk bootstrap` の完了 5 | - 上記のコマンドが正常に実行されると、cdk がリソースの作成に利用する IAM Role が作成されます。 6 | 7 | ## GitHub Actions で利用する AWS 権限の設定 8 | 9 | 本レポジトリで利用されている GitHUb Actions の CI/CD では、AWS のリソース作成時に利用する権限を、OIDC (OpenID Connect) 連携にて GitHub Actions の実行環境に渡しています。 10 | 11 | 以下ではその設定について説明します。詳細は [GitHub のドキュメント](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)や [AWS のドキュメント](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)をご参照ください。 12 | 13 | ### AWS Identity and Access Management (AWS Identity and Access Management) の IP プロバイダの作成 14 | 15 | AWS マネジメントコンソールの検索画面で、IAM を検索、選択します。 16 | 17 | ダッシュボードの左側のメニュー (閉じていたら三本線をクリックして開いてください。)から、🔽アクセス管理から、ID プロバイダを選択します。 18 | 19 | 「プロバイダを追加」をクリックします。 20 | 21 | 遷移後の画面にて、項目をそれぞれ以下のように入力します。 22 | - プロバイダのタイプ: OpenID Connect をチェック 23 | - プロバイダの URL: https://token.actions.githubusercontent.com 24 | - プロバイダの URL 入力後、その横にある「サムプリントを取得」をクリック 25 | - 対象者: sts.amazonaws.com 26 | 27 | 入力したら、「プロバイダを追加」をクリックします。 28 | 29 | 30 | ### ID プロバイダに割り当てる IAM Role の作成 31 | 上記の手順が完了した際に、コンソール上部にポップアップが現れるので、「ロールの割り当て」をクリックし、「新しいロールを作成」にチェックを入れて、「次へ」をクリックします。 32 | 33 | 項目をそれぞれ以下のように入力します。 34 | 35 | - 信頼されたエンティティタイプ: ウェブアイデンティティ 36 | - ウェブアイデンティティ: 37 | - アイデンティティプロバイダー: 前の手順でプロバイダの URL に指定した 「token.actions.githubusercontent.com」を選択 38 | - Audience: 前の手順で対象者に選択した「sts.amazonaws.com」を選択 39 | - GitHub 組織: 利用する GitHub のアカウント名 40 | - GitHub リポジトリ: 利用する GitHub のレポジトリ名 (オプション) 41 | - GitHub ブランチ: 利用する GitHub レポジトリのブランチ名 (オプション) 42 | 43 | 入力後、「ロールを作成」をクリックして次の画面に移ります。 44 | 45 | 許可を追加の場面では今は何もせず、そのまま「次へ」をクリックします。 46 | 47 | ロール名をとして `GitHubOidcRole` を入力し 、「ロールを作成」をクリックします。 48 | 49 | 作成後、ロールを表示し、許可ポリシーの「許可を追加 🔽」から「インラインポリシーを作成」を選択します。ポリシーエディタで `JSON` を選択して以下の内容を貼り付けます。`` 部分は置き換えてください。また、下記ポリシーでも動作しますが、内容は適切に絞ってください。 50 | 51 | ```json 52 | 53 | { 54 | "Version": "2012-10-17", 55 | "Statement": [ 56 | { 57 | "Sid": "Statement1", 58 | "Effect": "Allow", 59 | "Action": "sts:AssumeRole", 60 | "Resource": "arn:aws:iam:::role/cdk-*" 61 | } 62 | ] 63 | } 64 | 65 | 「次へ」をクリックして、ポリシーの詳細画面でポリシー名を `GitHubOidcPolicy` と入力します。「ポリシーを作成」をクリックします。 66 | 67 | 68 | ## GitHub 側での設定 69 | 70 | ### パラメータの作成 71 | GitHub のレポジトリにて、Setting タブを選択します。 72 | 遷移後、左側のメニュー下部にある 「Secrets and Valuable 🔽」から、「Actions」を選択します。 73 | 74 | 遷移後の画面だと 「Secrets」 タブが選択されているので、「Variables」に切り替えます。右側の「New Repository Variable」をクリックします。 75 | 以下のように入力します。 76 | 77 | - Name: `AWS_OIDC_ROLE_ARN` 78 | - Value: `arn:aws:iam:::role/GitHubOidcRole` 79 | - は置き換えてください 80 | - OIDC 用の Role 名に `GitHubOidcRole` 以外を入力した場合は、適宜読み替えてください。 81 | 82 | 83 | 84 | 85 | 86 | --- 87 | 88 | 以上で設定は完了です。これで ワークフロー内で処理を記述して、GitHub Actions から OIDC 連携にて、IAM Role を引き受けて、AWS へリソースをデプロイできます。 89 | -------------------------------------------------------------------------------- /imgs/README.md: -------------------------------------------------------------------------------- 1 | ## gif 作成方法 2 | 3 | ```bash 4 | ffmpeg -i input.mov -filter_complex "[0:v] fps=10,scale=640:-1,split [a][b];[a] palettegen [p];[b][p] paletteuse" output.gif 5 | ``` 6 | 7 | - [参考](https://qiita.com/yusuga/items/ba7b5c2cac3f2928f040) 8 | -------------------------------------------------------------------------------- /imgs/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/arch.png -------------------------------------------------------------------------------- /imgs/playground.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/playground.gif -------------------------------------------------------------------------------- /imgs/sc_lp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/sc_lp.png -------------------------------------------------------------------------------- /imgs/usecase_chat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/usecase_chat.gif -------------------------------------------------------------------------------- /imgs/usecase_editorial.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/usecase_editorial.gif -------------------------------------------------------------------------------- /imgs/usecase_generate_text.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/usecase_generate_text.gif -------------------------------------------------------------------------------- /imgs/usecase_rag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/usecase_rag.gif -------------------------------------------------------------------------------- /imgs/usecase_summarize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/usecase_summarize.gif -------------------------------------------------------------------------------- /imgs/usecase_translate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/imgs/usecase_translate.gif -------------------------------------------------------------------------------- /oidc-setup.yaml: -------------------------------------------------------------------------------- 1 | Parameters: 2 | GithubOrg: 3 | Type: String 4 | Default: example-org 5 | RepoName: 6 | Type: String 7 | Default: cfn-cicd 8 | RoleName: 9 | Type: String 10 | Default: GitHubOidcRole 11 | 12 | Resources: 13 | Role: 14 | Type: AWS::IAM::Role 15 | Properties: 16 | RoleName: !Ref RoleName 17 | AssumeRolePolicyDocument: 18 | Statement: 19 | - Effect: Allow 20 | Action: sts:AssumeRoleWithWebIdentity 21 | Principal: 22 | Federated: !Ref GithubOidc 23 | Condition: 24 | StringLike: 25 | token.actions.githubusercontent.com:sub: !Sub repo:${GithubOrg}/${RepoName}:* 26 | Policies: 27 | - PolicyName: GitHubOidcPolicy 28 | PolicyDocument: 29 | Version: '2012-10-17' 30 | Statement: 31 | - Effect: Allow 32 | Action: sts:AssumeRole 33 | Resource: !Sub arn:aws:iam::${AWS::AccountId}:role/cdk-* 34 | 35 | GithubOidc: 36 | Type: AWS::IAM::OIDCProvider 37 | Properties: 38 | Url: https://token.actions.githubusercontent.com 39 | ThumbprintList: [1b511abead59c6ce207077c0bf0e0043b1382612] 40 | ClientIdList: 41 | - sts.amazonaws.com 42 | 43 | Outputs: 44 | Role: 45 | Value: !GetAtt Role.Arn -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generative-ai-use-cases-jp", 3 | "private": true, 4 | "version": "0.0.1", 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 && npm -w packages/web run dev", 9 | "web:dev": "npm -w packages/web run dev", 10 | "web:build": "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 --", 13 | "cdk:watch": "npm -w packages/cdk run cdk watch --", 14 | "cdk:lint": "npm -w packages/cdk run lint" 15 | }, 16 | "devDependencies": { 17 | "@tailwindcss/forms": "^0.5.4", 18 | "npm-run-all": "^4.1.5", 19 | "prettier": "^3.0.0", 20 | "prettier-plugin-tailwindcss": "^0.4.1", 21 | "tailwind-scrollbar": "^3.0.5" 22 | }, 23 | "workspaces": [ 24 | "packages/*" 25 | ] 26 | } -------------------------------------------------------------------------------- /package_models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Get Model Folders 4 | MODEL_FOLDERS=$(ls -d packages/cdk/models/*/) 5 | 6 | # Iterate Model Folders 7 | for MODEL_DIR in $MODEL_FOLDERS 8 | do 9 | MODEL_FILE=${MODEL_DIR%/}.tar.gz 10 | 11 | echo "==--------Packing $MODEL_FILE---------==" 12 | 13 | if [ -f "$MODEL_FILE" ]; then 14 | rm $MODEL_FILE 15 | fi 16 | 17 | tar -zcvf $MODEL_FILE -C $MODEL_DIR . 18 | 19 | done 20 | -------------------------------------------------------------------------------- /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/bin/generative-ai-use-cases.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { GenerativeAiUseCasesStack } from '../lib/generative-ai-use-cases-stack'; 5 | 6 | const app = new cdk.App(); 7 | 8 | const stage = app.node.tryGetContext('stage') || ''; 9 | 10 | new GenerativeAiUseCasesStack(app, `${stage}GenerativeAiUseCasesStack`); 11 | -------------------------------------------------------------------------------- /packages/cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/generative-ai-use-cases.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 | "ragEnabled": false, 21 | "selfSignUpEnabled": false, 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/core:checkSecretUsage": true, 24 | "@aws-cdk/core:target-partitions": [ 25 | "aws", 26 | "aws-cn" 27 | ], 28 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 29 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/aws-iam:minimizePolicies": true, 32 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 33 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 34 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 35 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 36 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 37 | "@aws-cdk/core:enablePartitionLiterals": true, 38 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 39 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 40 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 41 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 42 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 43 | "@aws-cdk/aws-route53-patters:useCertificate": true, 44 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 45 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 46 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 47 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 48 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 49 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 50 | "@aws-cdk/aws-redshift:columnId": true, 51 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 52 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 53 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 54 | "@aws-cdk/aws-kms:aliasNameRef": true, 55 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 56 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 57 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /packages/cdk/lambda/checkEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyResult } from 'aws-lambda'; 2 | import { SageMaker } from '@aws-sdk/client-sagemaker'; 3 | 4 | const sagemaker = new SageMaker(); 5 | const endpointName = process.env.ENDPOINT_NAME; 6 | 7 | exports.handler = async (): Promise => { 8 | try { 9 | // Get the status of the SageMaker endpoint 10 | const describeEndpointResponse = await sagemaker.describeEndpoint({ 11 | EndpointName: endpointName || '', 12 | }); 13 | 14 | const endpointStatus = describeEndpointResponse.EndpointStatus; 15 | 16 | return createResponse(200, { 17 | EndpointStatus: endpointStatus, 18 | }); 19 | } catch (error: unknown) { 20 | if (error instanceof Error) { 21 | console.error(`Error: ${error.message}`); 22 | } else { 23 | console.error(`Error: ${error}`); 24 | } 25 | return createResponse(200, { 26 | EndpointStatus: 'OutOfService', 27 | }); 28 | } 29 | }; 30 | 31 | function createResponse( 32 | statusCode: number, 33 | body: object 34 | ): APIGatewayProxyResult { 35 | return { 36 | statusCode, 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | 'Access-Control-Allow-Origin': '*', 40 | }, 41 | body: JSON.stringify(body), 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/cdk/lambda/createChat.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { createChat } from './repository'; 3 | import { Logger } from '@aws-lambda-powertools/logger'; 4 | 5 | const logger = new Logger(); 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | try { 11 | const userId: string = 12 | event.requestContext.authorizer!.claims['cognito:username']; 13 | const chat = await createChat(userId); 14 | 15 | return { 16 | statusCode: 200, 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | 'Access-Control-Allow-Origin': '*', 20 | }, 21 | body: JSON.stringify({ 22 | chat, 23 | }), 24 | }; 25 | } catch (error) { 26 | logger.error(error); 27 | return { 28 | statusCode: 500, 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | 'Access-Control-Allow-Origin': '*', 32 | }, 33 | body: JSON.stringify({ message: 'Internal Server Error' }), 34 | }; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cdk/lambda/createEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { SageMaker } from '@aws-sdk/client-sagemaker'; 3 | import { Logger } from '@aws-lambda-powertools/logger'; 4 | 5 | const logger = new Logger(); 6 | 7 | const sagemaker = new SageMaker(); 8 | const endpointConfigName = process.env.ENDPOINT_CONFIG_NAME; 9 | const endpointName = process.env.ENDPOINT_NAME; 10 | 11 | exports.handler = async ( 12 | event: APIGatewayProxyEvent 13 | ): Promise => { 14 | try { 15 | const requestType = event.httpMethod; 16 | 17 | if (requestType === 'POST') { 18 | // Create the SageMaker endpoint 19 | const createEndpointResponse = await sagemaker.createEndpoint({ 20 | EndpointName: endpointName, 21 | EndpointConfigName: endpointConfigName, 22 | }); 23 | 24 | logger.info( 25 | `SageMaker endpoint created: ${createEndpointResponse.EndpointArn}` 26 | ); 27 | return createResponse(200, { 28 | Message: 'SageMaker endpoint created', 29 | EndpointArn: createEndpointResponse.EndpointArn, 30 | }); 31 | } else if (requestType === 'DELETE') { 32 | // Delete the SageMaker endpoint 33 | await sagemaker.deleteEndpoint({ EndpointName: endpointName }); 34 | logger.info(`SageMaker endpoint deleted: ${endpointName}`); 35 | return createResponse(200, { 36 | Message: 'SageMaker endpoint deleted', 37 | }); 38 | } else { 39 | return createResponse(400, { 40 | Message: 'Unsupported Method', 41 | }); 42 | } 43 | } catch (error: unknown) { 44 | if (error instanceof Error) { 45 | console.error(`Error: ${error.message}`); 46 | } else { 47 | console.error(`Error: ${error}`); 48 | } 49 | return createResponse(500, { 50 | Message: 'Internal Server Error', 51 | }); 52 | } 53 | }; 54 | 55 | function createResponse( 56 | statusCode: number, 57 | body: object 58 | ): APIGatewayProxyResult { 59 | return { 60 | statusCode, 61 | headers: { 62 | 'Content-Type': 'application/json', 63 | 'Access-Control-Allow-Origin': '*', 64 | }, 65 | body: JSON.stringify(body), 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/cdk/lambda/createMessages.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { CreateMessagesRequest } from 'generative-ai-use-cases-jp'; 3 | import { batchCreateMessages } from './repository'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | 6 | const logger = new Logger(); 7 | 8 | export const handler = async ( 9 | event: APIGatewayProxyEvent 10 | ): Promise => { 11 | try { 12 | const req: CreateMessagesRequest = JSON.parse(event.body!); 13 | const userId: string = 14 | event.requestContext.authorizer!.claims['cognito:username']; 15 | const chatId = event.pathParameters!.chatId!; 16 | const messages = await batchCreateMessages(req.messages, userId, chatId); 17 | 18 | return { 19 | statusCode: 200, 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | 'Access-Control-Allow-Origin': '*', 23 | }, 24 | body: JSON.stringify({ 25 | messages, 26 | }), 27 | }; 28 | } catch (error: unknown) { 29 | if (error instanceof Error) logger.error(error.message); 30 | return { 31 | statusCode: 500, 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | 'Access-Control-Allow-Origin': '*', 35 | }, 36 | body: JSON.stringify({ message: 'Internal Server Error' }), 37 | }; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /packages/cdk/lambda/deleteChat.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { deleteChat } from './repository'; 3 | import { Logger } from '@aws-lambda-powertools/logger'; 4 | 5 | const logger = new Logger(); 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | try { 11 | const userId: string = 12 | event.requestContext.authorizer!.claims['cognito:username']; 13 | const chatId = event.pathParameters!.chatId!; 14 | await deleteChat(userId, chatId); 15 | 16 | return { 17 | statusCode: 204, 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | 'Access-Control-Allow-Origin': '*', 21 | }, 22 | body: '', 23 | }; 24 | } catch (error: unknown) { 25 | if (error instanceof Error) logger.error(error.message); 26 | return { 27 | statusCode: 500, 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | 'Access-Control-Allow-Origin': '*', 31 | }, 32 | body: JSON.stringify({ message: 'Internal Server Error' }), 33 | }; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /packages/cdk/lambda/deleteEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { SageMaker } from '@aws-sdk/client-sagemaker'; 2 | import { Logger } from '@aws-lambda-powertools/logger'; 3 | 4 | const logger = new Logger(); 5 | 6 | const sagemaker = new SageMaker(); 7 | const endpointName = process.env.ENDPOINT_NAME; 8 | 9 | exports.handler = async () => { 10 | try { 11 | // Delete the SageMaker endpoint 12 | await sagemaker.deleteEndpoint({ EndpointName: endpointName }); 13 | logger.info(`SageMaker endpoint deleted: ${endpointName}`); 14 | } catch (error: unknown) { 15 | if (error instanceof Error) { 16 | console.error(`Error: ${error.message}`); 17 | } else { 18 | console.error(`Error: ${error}`); 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cdk/lambda/findChatById.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { findChatById } from './repository'; 3 | import { Logger } from '@aws-lambda-powertools/logger'; 4 | 5 | const logger = new Logger(); 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | try { 11 | const userId: string = 12 | event.requestContext.authorizer!.claims['cognito:username']; 13 | const chatId = event.pathParameters!.chatId!; 14 | const chat = await findChatById(userId, chatId); 15 | 16 | return { 17 | statusCode: 200, 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | 'Access-Control-Allow-Origin': '*', 21 | }, 22 | body: JSON.stringify({ 23 | chat, 24 | }), 25 | }; 26 | } catch (error: unknown) { 27 | if (error instanceof Error) logger.error(error.message); 28 | return { 29 | statusCode: 500, 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | 'Access-Control-Allow-Origin': '*', 33 | }, 34 | body: JSON.stringify({ message: 'Internal Server Error' }), 35 | }; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /packages/cdk/lambda/listChats.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { listChats } from './repository'; 3 | import { Logger } from '@aws-lambda-powertools/logger'; 4 | 5 | const logger = new Logger(); 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | try { 11 | const userId: string = 12 | event.requestContext.authorizer!.claims['cognito:username']; 13 | const chats = await listChats(userId); 14 | 15 | return { 16 | statusCode: 200, 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | 'Access-Control-Allow-Origin': '*', 20 | }, 21 | body: JSON.stringify({ 22 | chats, 23 | }), 24 | }; 25 | } catch (error: unknown) { 26 | if (error instanceof Error) logger.error(error.message); 27 | return { 28 | statusCode: 500, 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | 'Access-Control-Allow-Origin': '*', 32 | }, 33 | body: JSON.stringify({ message: 'Internal Server Error' }), 34 | }; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cdk/lambda/listMessages.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { findChatById, listMessages } from './repository'; 3 | import { Logger } from '@aws-lambda-powertools/logger'; 4 | 5 | const logger = new Logger(); 6 | 7 | export const handler = async ( 8 | event: APIGatewayProxyEvent 9 | ): Promise => { 10 | try { 11 | const userId: string = 12 | event.requestContext.authorizer!.claims['cognito:username']; 13 | const chatId = event.pathParameters!.chatId!; 14 | const chat = await findChatById(userId, chatId); 15 | 16 | if (chat === null) { 17 | return { 18 | statusCode: 403, 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | 'Access-Control-Allow-Origin': '*', 22 | }, 23 | body: JSON.stringify({ message: 'Forbidden' }), 24 | }; 25 | } 26 | 27 | const messages = await listMessages(chatId); 28 | 29 | return { 30 | statusCode: 200, 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | 'Access-Control-Allow-Origin': '*', 34 | }, 35 | body: JSON.stringify({ 36 | messages, 37 | }), 38 | }; 39 | } catch (error: unknown) { 40 | if (error instanceof Error) logger.error(error.message); 41 | return { 42 | statusCode: 500, 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | 'Access-Control-Allow-Origin': '*', 46 | }, 47 | body: JSON.stringify({ message: 'Internal Server Error' }), 48 | }; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /packages/cdk/lambda/predict.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { PredictRequest } from 'generative-ai-use-cases-jp'; 3 | import sagemakerApi from './utils/sagemakerApi'; 4 | import bedrockApi from './utils/bedrockApi'; 5 | import { Logger } from '@aws-lambda-powertools/logger'; 6 | 7 | const logger = new Logger(); 8 | 9 | const modelType = process.env.MODEL_TYPE || 'bedrock'; 10 | const api = 11 | { 12 | bedrock: bedrockApi, 13 | sagemaker: sagemakerApi, 14 | }[modelType] || bedrockApi; 15 | 16 | export const handler = async ( 17 | event: APIGatewayProxyEvent 18 | ): Promise => { 19 | try { 20 | const req: PredictRequest = JSON.parse(event.body!); 21 | const response = await api.invoke(req.inputs); 22 | 23 | return { 24 | statusCode: 200, 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | 'Access-Control-Allow-Origin': '*', 28 | }, 29 | body: JSON.stringify(response), 30 | }; 31 | } catch (error: unknown) { 32 | if (error instanceof Error) logger.error(error.message); 33 | return { 34 | statusCode: 500, 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | 'Access-Control-Allow-Origin': '*', 38 | }, 39 | body: JSON.stringify({ message: 'Internal Server Error' }), 40 | }; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/cdk/lambda/predictStream.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from 'aws-lambda'; 2 | import { PredictRequest } from 'generative-ai-use-cases-jp'; 3 | import sagemakerApi from './utils/sagemakerApi'; 4 | import bedrockApi from './utils/bedrockApi'; 5 | 6 | const modelType = process.env.MODEL_TYPE || 'bedrock'; 7 | const api = 8 | { 9 | bedrock: bedrockApi, 10 | sagemaker: sagemakerApi, 11 | }[modelType] || bedrockApi; 12 | 13 | declare global { 14 | namespace awslambda { 15 | function streamifyResponse( 16 | f: ( 17 | event: PredictRequest, 18 | responseStream: NodeJS.WritableStream 19 | ) => Promise 20 | ): Handler; 21 | } 22 | } 23 | 24 | export const handler = awslambda.streamifyResponse( 25 | async (event, responseStream) => { 26 | for await (const token of api.invokeStream(event.inputs, event.params)) { 27 | responseStream.write(token); 28 | } 29 | 30 | responseStream.end(); 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /packages/cdk/lambda/predictTitle.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { PredictTitleRequest } from 'generative-ai-use-cases-jp'; 3 | import { setChatTitle } from './repository'; 4 | import sagemakerApi from './utils/sagemakerApi'; 5 | import bedrockApi from './utils/bedrockApi'; 6 | import { Logger } from '@aws-lambda-powertools/logger'; 7 | 8 | const logger = new Logger(); 9 | 10 | const modelType = process.env.MODEL_TYPE || 'bedrock'; 11 | const api = 12 | { 13 | bedrock: bedrockApi, 14 | sagemaker: sagemakerApi, 15 | }[modelType] || bedrockApi; 16 | 17 | export const handler = async ( 18 | event: APIGatewayProxyEvent 19 | ): Promise => { 20 | try { 21 | const req: PredictTitleRequest = JSON.parse(event.body!); 22 | 23 | let title = ''; 24 | for await (const token of api.invokeStream(req.inputs)) { 25 | title += token.replace(req.eos_token, '').trim(); 26 | } 27 | 28 | await setChatTitle(req.chat.id, req.chat.createdDate, title); 29 | 30 | return { 31 | statusCode: 200, 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | 'Access-Control-Allow-Origin': '*', 35 | }, 36 | body: title, 37 | }; 38 | } catch (error: unknown) { 39 | if (error instanceof Error) logger.error(error.message); 40 | return { 41 | statusCode: 500, 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | 'Access-Control-Allow-Origin': '*', 45 | }, 46 | body: JSON.stringify({ message: 'Internal Server Error' }), 47 | }; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /packages/cdk/lambda/queryKendra.ts: -------------------------------------------------------------------------------- 1 | import * as lambda from 'aws-lambda'; 2 | import { 3 | AttributeFilter, 4 | KendraClient, 5 | QueryCommand, 6 | } from '@aws-sdk/client-kendra'; 7 | import { QueryKendraRequest } from 'generative-ai-use-cases-jp'; 8 | 9 | const INDEX_ID = process.env.INDEX_ID; 10 | 11 | exports.handler = async ( 12 | event: lambda.APIGatewayProxyEvent 13 | ): Promise => { 14 | const req = JSON.parse(event.body!) as QueryKendraRequest; 15 | const query = req.query; 16 | 17 | if (!query) { 18 | return { 19 | statusCode: 400, 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | 'Access-Control-Allow-Origin': '*', 23 | }, 24 | body: JSON.stringify({ error: 'query is not specified' }), 25 | }; 26 | } 27 | 28 | // デフォルト言語が英語なので、言語設定は必ず行う 29 | const attributeFilter: AttributeFilter = { 30 | AndAllFilters: [ 31 | { 32 | EqualsTo: { 33 | Key: '_language_code', 34 | Value: { 35 | StringValue: 'ja', 36 | }, 37 | }, 38 | }, 39 | ], 40 | }; 41 | 42 | const kendra = new KendraClient({}); 43 | const queryCommand = new QueryCommand({ 44 | IndexId: INDEX_ID, 45 | QueryText: query, 46 | AttributeFilter: attributeFilter, 47 | }); 48 | 49 | const queryRes = await kendra.send(queryCommand); 50 | 51 | return { 52 | statusCode: 200, 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | 'Access-Control-Allow-Origin': '*', 56 | }, 57 | body: JSON.stringify(queryRes), 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/cdk/lambda/repository.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chat, 3 | RecordedMessage, 4 | ToBeRecordedMessage, 5 | } from 'generative-ai-use-cases-jp'; 6 | import * as crypto from 'crypto'; 7 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 8 | import { 9 | BatchWriteCommand, 10 | DeleteCommand, 11 | DynamoDBDocumentClient, 12 | PutCommand, 13 | QueryCommand, 14 | UpdateCommand, 15 | } from '@aws-sdk/lib-dynamodb'; 16 | 17 | const TABLE_NAME: string = process.env.TABLE_NAME!; 18 | const dynamoDb = new DynamoDBClient({}); 19 | const dynamoDbDocument = DynamoDBDocumentClient.from(dynamoDb); 20 | 21 | export const createChat = async (_userId: string): Promise => { 22 | const userId = `user#${_userId}`; 23 | const chatId = `chat#${crypto.randomUUID()}`; 24 | const item = { 25 | id: userId, 26 | createdDate: `${Date.now()}`, 27 | chatId, 28 | usecase: '', 29 | title: '', 30 | updatedDate: '', 31 | }; 32 | 33 | await dynamoDbDocument.send( 34 | new PutCommand({ 35 | TableName: TABLE_NAME, 36 | Item: item, 37 | }) 38 | ); 39 | 40 | return item; 41 | }; 42 | 43 | export const findChatById = async ( 44 | _userId: string, 45 | _chatId: string 46 | ): Promise => { 47 | const userId = `user#${_userId}`; 48 | const chatId = `chat#${_chatId}`; 49 | const res = await dynamoDbDocument.send( 50 | new QueryCommand({ 51 | TableName: TABLE_NAME, 52 | KeyConditionExpression: '#id = :id', 53 | FilterExpression: '#chatId = :chatId', 54 | ExpressionAttributeNames: { 55 | '#id': 'id', 56 | '#chatId': 'chatId', 57 | }, 58 | ExpressionAttributeValues: { 59 | ':id': userId, 60 | ':chatId': chatId, 61 | }, 62 | }) 63 | ); 64 | 65 | if (!res.Items || res.Items.length === 0) { 66 | return null; 67 | } else { 68 | return res.Items[0] as Chat; 69 | } 70 | }; 71 | 72 | export const listChats = async (_userId: string): Promise => { 73 | const userId = `user#${_userId}`; 74 | const res = await dynamoDbDocument.send( 75 | new QueryCommand({ 76 | TableName: TABLE_NAME, 77 | KeyConditionExpression: '#id = :id', 78 | ExpressionAttributeNames: { 79 | '#id': 'id', 80 | }, 81 | ExpressionAttributeValues: { 82 | ':id': userId, 83 | }, 84 | ScanIndexForward: false, 85 | }) 86 | ); 87 | 88 | return res.Items as Chat[]; 89 | }; 90 | 91 | export const listMessages = async ( 92 | _chatId: string 93 | ): Promise => { 94 | const chatId = `chat#${_chatId}`; 95 | const res = await dynamoDbDocument.send( 96 | new QueryCommand({ 97 | TableName: TABLE_NAME, 98 | KeyConditionExpression: '#id = :id', 99 | ExpressionAttributeNames: { 100 | '#id': 'id', 101 | }, 102 | ExpressionAttributeValues: { 103 | ':id': chatId, 104 | }, 105 | }) 106 | ); 107 | 108 | return res.Items as RecordedMessage[]; 109 | }; 110 | 111 | export const batchCreateMessages = async ( 112 | messages: ToBeRecordedMessage[], 113 | _userId: string, 114 | _chatId: string 115 | ): Promise => { 116 | const userId = `user#${_userId}`; 117 | const chatId = `chat#${_chatId}`; 118 | const createdDate = Date.now(); 119 | const feedback = 'none'; 120 | const usecase = ''; 121 | const llmType = 'bedrock'; 122 | 123 | const items: RecordedMessage[] = messages.map( 124 | (m: ToBeRecordedMessage, i: number) => { 125 | return { 126 | id: chatId, 127 | createdDate: `${createdDate + i}#0`, 128 | messageId: m.messageId, 129 | role: m.role, 130 | content: m.content, 131 | userId, 132 | feedback, 133 | usecase, 134 | llmType, 135 | }; 136 | } 137 | ); 138 | 139 | await dynamoDbDocument.send( 140 | new BatchWriteCommand({ 141 | RequestItems: { 142 | [TABLE_NAME]: items.map((m) => { 143 | return { 144 | PutRequest: { 145 | Item: m, 146 | }, 147 | }; 148 | }), 149 | }, 150 | }) 151 | ); 152 | 153 | return items; 154 | }; 155 | 156 | export const setChatTitle = async ( 157 | id: string, 158 | createdDate: string, 159 | title: string 160 | ) => { 161 | const res = await dynamoDbDocument.send( 162 | new UpdateCommand({ 163 | TableName: TABLE_NAME, 164 | Key: { 165 | id: id, 166 | createdDate: createdDate, 167 | }, 168 | UpdateExpression: 'set title = :title', 169 | ExpressionAttributeValues: { 170 | ':title': title, 171 | }, 172 | ReturnValues: 'ALL_NEW', 173 | }) 174 | ); 175 | return res.Attributes as Chat; 176 | }; 177 | 178 | export const updateFeedback = async ( 179 | _chatId: string, 180 | createdDate: string, 181 | feedback: string 182 | ): Promise => { 183 | const chatId = `chat#${_chatId}`; 184 | const res = await dynamoDbDocument.send( 185 | new UpdateCommand({ 186 | TableName: TABLE_NAME, 187 | Key: { 188 | id: chatId, 189 | createdDate, 190 | }, 191 | UpdateExpression: 'set feedback = :feedback', 192 | ExpressionAttributeValues: { 193 | ':feedback': feedback, 194 | }, 195 | ReturnValues: 'ALL_NEW', 196 | }) 197 | ); 198 | 199 | return res.Attributes as RecordedMessage; 200 | }; 201 | 202 | export const deleteChat = async ( 203 | _userId: string, 204 | _chatId: string 205 | ): Promise => { 206 | // Chat の削除 207 | const chatItem = await findChatById(_userId, _chatId); 208 | await dynamoDbDocument.send( 209 | new DeleteCommand({ 210 | TableName: TABLE_NAME, 211 | Key: { 212 | id: chatItem?.id, 213 | createdDate: chatItem?.createdDate, 214 | }, 215 | }) 216 | ); 217 | 218 | // // Message の削除 219 | const messageItems = await listMessages(_chatId); 220 | await dynamoDbDocument.send( 221 | new BatchWriteCommand({ 222 | RequestItems: { 223 | [TABLE_NAME]: messageItems.map((m) => { 224 | return { 225 | DeleteRequest: { 226 | Key: { 227 | id: m.id, 228 | createdDate: m.createdDate, 229 | }, 230 | }, 231 | }; 232 | }), 233 | }, 234 | }) 235 | ); 236 | }; 237 | -------------------------------------------------------------------------------- /packages/cdk/lambda/retrieveKendra.ts: -------------------------------------------------------------------------------- 1 | import * as lambda from 'aws-lambda'; 2 | import { 3 | AttributeFilter, 4 | KendraClient, 5 | RetrieveCommand, 6 | } from '@aws-sdk/client-kendra'; 7 | import { RetrieveKendraRequest } from 'generative-ai-use-cases-jp'; 8 | 9 | const INDEX_ID = process.env.INDEX_ID; 10 | 11 | exports.handler = async ( 12 | event: lambda.APIGatewayProxyEvent 13 | ): Promise => { 14 | const req = JSON.parse(event.body!) as RetrieveKendraRequest; 15 | const query = req.query; 16 | 17 | if (!query) { 18 | return { 19 | statusCode: 400, 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | 'Access-Control-Allow-Origin': '*', 23 | }, 24 | body: JSON.stringify({ error: 'query is not specified' }), 25 | }; 26 | } 27 | 28 | // デフォルト言語が英語なので、言語設定は必ず行う 29 | const attributeFilter: AttributeFilter = { 30 | AndAllFilters: [ 31 | { 32 | EqualsTo: { 33 | Key: '_language_code', 34 | Value: { 35 | StringValue: 'ja', 36 | }, 37 | }, 38 | }, 39 | ], 40 | }; 41 | 42 | const kendra = new KendraClient({}); 43 | const retrieveCommand = new RetrieveCommand({ 44 | IndexId: INDEX_ID, 45 | QueryText: query, 46 | AttributeFilter: attributeFilter, 47 | }); 48 | 49 | const retrieveRes = await kendra.send(retrieveCommand); 50 | 51 | return { 52 | statusCode: 200, 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | 'Access-Control-Allow-Origin': '*', 56 | }, 57 | body: JSON.stringify(retrieveRes), 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/cdk/lambda/updateFeedback.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { UpdateFeedbackRequest } from 'generative-ai-use-cases-jp'; 3 | import { updateFeedback } from './repository'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | 6 | const logger = new Logger(); 7 | 8 | export const handler = async ( 9 | event: APIGatewayProxyEvent 10 | ): Promise => { 11 | try { 12 | const chatId = event.pathParameters!.chatId!; 13 | const req: UpdateFeedbackRequest = JSON.parse(event.body!); 14 | 15 | const message = await updateFeedback(chatId, req.createdDate, req.feedback); 16 | 17 | return { 18 | statusCode: 200, 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | 'Access-Control-Allow-Origin': '*', 22 | }, 23 | body: JSON.stringify({ message }), 24 | }; 25 | } catch (error: unknown) { 26 | if (error instanceof Error) logger.error(error.message); 27 | return { 28 | statusCode: 500, 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | 'Access-Control-Allow-Origin': '*', 32 | }, 33 | body: JSON.stringify({ message: 'Internal Server Error' }), 34 | }; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/cdk/lambda/updateTitle.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; 2 | import { UpdateTitleRequest } from 'generative-ai-use-cases-jp'; 3 | import { findChatById, setChatTitle } from './repository'; 4 | import { Logger } from '@aws-lambda-powertools/logger'; 5 | 6 | const logger = new Logger(); 7 | 8 | export const handler = async ( 9 | event: APIGatewayProxyEvent 10 | ): Promise => { 11 | try { 12 | const userId: string = 13 | event.requestContext.authorizer!.claims['cognito:username']; 14 | const chatId = event.pathParameters!.chatId!; 15 | const req: UpdateTitleRequest = JSON.parse(event.body!); 16 | 17 | const chatItem = await findChatById(userId, chatId); 18 | 19 | if (!chatItem) { 20 | return { 21 | statusCode: 404, 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | 'Access-Control-Allow-Origin': '*', 25 | }, 26 | body: '', 27 | }; 28 | } 29 | 30 | const updatedChat = await setChatTitle( 31 | chatItem?.id, 32 | chatItem?.createdDate, 33 | req.title 34 | ); 35 | 36 | return { 37 | statusCode: 200, 38 | headers: { 39 | 'Content-Type': 'application/json', 40 | 'Access-Control-Allow-Origin': '*', 41 | }, 42 | body: JSON.stringify({ chat: updatedChat }), 43 | }; 44 | } catch (error: unknown) { 45 | if (error instanceof Error) logger.error(error.message); 46 | return { 47 | statusCode: 500, 48 | headers: { 49 | 'Content-Type': 'application/json', 50 | 'Access-Control-Allow-Origin': '*', 51 | }, 52 | body: JSON.stringify({ message: 'Internal Server Error' }), 53 | }; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /packages/cdk/lambda/utils/bedrockApi.ts: -------------------------------------------------------------------------------- 1 | import { PredictParams } from 'generative-ai-use-cases-jp'; 2 | import { 3 | BedrockRuntimeClient, 4 | InvokeModelCommand, 5 | InvokeModelWithResponseStreamCommand, 6 | } from '@aws-sdk/client-bedrock-runtime'; 7 | 8 | const client = new BedrockRuntimeClient({ 9 | region: process.env.MODEL_REGION, 10 | }); 11 | 12 | const PARAMS = { 13 | max_tokens_to_sample: 3000, 14 | temperature: 0.6, 15 | top_k: 300, 16 | top_p: 0.8, 17 | }; 18 | 19 | const invoke = async ( 20 | inputs: string, 21 | params: PredictParams = {} 22 | ): Promise => { 23 | const command = new InvokeModelCommand({ 24 | modelId: process.env.MODEL_NAME, 25 | body: JSON.stringify({ 26 | prompt: inputs, // generatePrompt(messages), 27 | ...{ ...PARAMS, params }, 28 | }), 29 | contentType: 'application/json', 30 | }); 31 | const data = await client.send(command); 32 | return JSON.parse(data.body.transformToString()).completion; 33 | }; 34 | 35 | async function* invokeStream( 36 | inputs: string, 37 | params: PredictParams = {} 38 | ): AsyncIterable { 39 | const command = new InvokeModelWithResponseStreamCommand({ 40 | modelId: process.env.MODEL_NAME, 41 | body: JSON.stringify({ 42 | prompt: inputs, //generatePrompt(messages), 43 | ...{ ...PARAMS, params }, 44 | }), 45 | contentType: 'application/json', 46 | }); 47 | const res = await client.send(command); 48 | 49 | if (!res.body) { 50 | return; 51 | } 52 | 53 | for await (const streamChunk of res.body) { 54 | if (!streamChunk.chunk?.bytes) { 55 | break; 56 | } 57 | const body = JSON.parse( 58 | new TextDecoder('utf-8').decode(streamChunk.chunk?.bytes) 59 | ); 60 | if (body.completion) { 61 | yield body.completion; 62 | } 63 | if (body.stop_reason) { 64 | break; 65 | } 66 | } 67 | } 68 | 69 | export default { 70 | invoke, 71 | invokeStream, 72 | }; 73 | -------------------------------------------------------------------------------- /packages/cdk/lambda/utils/prompter.ts: -------------------------------------------------------------------------------- 1 | import { PromptTemplate, UnrecordedMessage } from 'generative-ai-use-cases-jp'; 2 | 3 | export const pt: PromptTemplate = JSON.parse(process.env.PROMPT_TEMPLATE || ''); 4 | 5 | export const generatePrompt = (messages: UnrecordedMessage[]) => { 6 | const prompt = 7 | pt.prefix + 8 | messages 9 | .map((message) => { 10 | if (message.role == 'user') { 11 | return pt.user.replace('{}', message.content); 12 | } else if (message.role == 'assistant') { 13 | return pt.assistant.replace('{}', message.content); 14 | } else if (message.role === 'system') { 15 | return pt.system.replace('{}', message.content); 16 | } else { 17 | throw new Error(`Invalid message role: ${message.role}`); 18 | } 19 | }) 20 | .join(pt.join) + 21 | pt.suffix; 22 | return prompt; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/cdk/lambda/utils/sagemakerApi.ts: -------------------------------------------------------------------------------- 1 | import { PredictParams } from 'generative-ai-use-cases-jp'; 2 | import { 3 | SageMakerRuntimeClient, 4 | InvokeEndpointCommand, 5 | InvokeEndpointWithResponseStreamCommand, 6 | } from '@aws-sdk/client-sagemaker-runtime'; 7 | import { Logger } from '@aws-lambda-powertools/logger'; 8 | 9 | const logger = new Logger(); 10 | 11 | const client = new SageMakerRuntimeClient({ 12 | region: process.env.MODEL_REGION, 13 | }); 14 | 15 | const PARAMS = { 16 | max_new_tokens: 512, 17 | return_full_text: false, 18 | do_sample: true, 19 | temperature: 0.3, 20 | }; 21 | 22 | const invoke = async ( 23 | inputs: string, 24 | params: PredictParams = {} 25 | ): Promise => { 26 | const variant = params.variant; 27 | delete params['variant']; 28 | logger.debug(inputs, params); 29 | const command = new InvokeEndpointCommand({ 30 | EndpointName: process.env.MODEL_NAME, 31 | TargetVariant: variant, 32 | Body: JSON.stringify({ 33 | inputs: inputs, 34 | parameters: { ...PARAMS, ...params }, 35 | }), 36 | ContentType: 'application/json', 37 | Accept: 'application/json', 38 | }); 39 | const data = await client.send(command); 40 | return JSON.parse(new TextDecoder().decode(data.Body))[0].generated_text; 41 | }; 42 | 43 | async function* invokeStream( 44 | inputs: string, 45 | params: PredictParams = {} 46 | ): AsyncIterable { 47 | const variant = params.variant; 48 | delete params['variant']; 49 | logger.debug(inputs, params); 50 | const command = new InvokeEndpointWithResponseStreamCommand({ 51 | EndpointName: process.env.MODEL_NAME, 52 | TargetVariant: variant, 53 | Body: JSON.stringify({ 54 | inputs: inputs, 55 | parameters: { ...PARAMS, ...params }, 56 | // stream: true, 57 | }), 58 | ContentType: 'application/json', 59 | Accept: 'application/json', 60 | }); 61 | const stream = (await client.send(command)).Body; 62 | if (!stream) return; 63 | 64 | // https://aws.amazon.com/blogs/machine-learning/elevating-the-generative-ai-experience-introducing-streaming-support-in-amazon-sagemaker-hosting/ 65 | // The output of the model will be in the following format: 66 | // b'data:{"token": {"text": " a"}}\n\n' 67 | // b'data:{"token": {"text": " challenging"}}\n\n' 68 | // b'data:{"token": {"text": " problem" 69 | // b'}}' 70 | // 71 | // While usually each PayloadPart event from the event stream will contain a byte array 72 | // with a full json, this is not guaranteed and some of the json objects may be split across 73 | // PayloadPart events. For example: 74 | // {'PayloadPart': {'Bytes': b'{"outputs": '}} 75 | // {'PayloadPart': {'Bytes': b'[" problem"]}\n'}} 76 | // 77 | // This logic accounts for this by concatenating bytes and 78 | // return lines (ending with a '\n' character) within the buffer. 79 | // It will also save any pending lines that doe not end with a '\n' 80 | // to make sure truncations are concatinated. 81 | 82 | let buffer = ''; 83 | for await (const chunk of stream) { 84 | buffer += new TextDecoder().decode(chunk.PayloadPart?.Bytes); 85 | if (!buffer.endsWith('\n')) continue; 86 | 87 | // When buffer end with \n it can be parsed 88 | const lines: string[] = 89 | buffer.split('\n').filter( 90 | (line: string) => line.trim() //.startsWith('data:') 91 | ) || []; 92 | for (const line of lines) { 93 | // const message = line.replace(/^data:/, ''); 94 | // const token: string = JSON.parse(message).token?.text || ''; 95 | const token: string = JSON.parse(line).outputs[0] || ''; 96 | // if (!token.includes(pt.eos_token)) 97 | yield token; 98 | } 99 | buffer = ''; 100 | } 101 | } 102 | 103 | export default { 104 | invoke, 105 | invokeStream, 106 | }; 107 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/auth.ts: -------------------------------------------------------------------------------- 1 | import { Duration } from 'aws-cdk-lib'; 2 | import { UserPool, UserPoolClient } from 'aws-cdk-lib/aws-cognito'; 3 | import { 4 | IdentityPool, 5 | UserPoolAuthenticationProvider, 6 | } from '@aws-cdk/aws-cognito-identitypool-alpha'; 7 | import { Construct } from 'constructs'; 8 | 9 | export interface AuthProps { 10 | selfSignUpEnabled: boolean; 11 | } 12 | 13 | export class Auth extends Construct { 14 | readonly userPool: UserPool; 15 | readonly client: UserPoolClient; 16 | readonly idPool: IdentityPool; 17 | 18 | constructor(scope: Construct, id: string, props: AuthProps) { 19 | super(scope, id); 20 | 21 | const userPool = new UserPool(this, 'UserPool', { 22 | selfSignUpEnabled: props.selfSignUpEnabled, 23 | signInAliases: { 24 | username: false, 25 | email: true, 26 | }, 27 | passwordPolicy: { 28 | requireUppercase: true, 29 | requireSymbols: true, 30 | requireDigits: true, 31 | minLength: 8, 32 | }, 33 | }); 34 | 35 | const client = userPool.addClient('client', { 36 | idTokenValidity: Duration.days(1), 37 | }); 38 | 39 | const idPool = new IdentityPool(this, 'IdentityPool', { 40 | authenticationProviders: { 41 | userPools: [ 42 | new UserPoolAuthenticationProvider({ 43 | userPool, 44 | userPoolClient: client, 45 | }), 46 | ], 47 | }, 48 | }); 49 | 50 | this.client = client; 51 | this.userPool = userPool; 52 | this.idPool = idPool; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/database.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as ddb from 'aws-cdk-lib/aws-dynamodb'; 3 | 4 | export class Database extends Construct { 5 | public readonly table: ddb.Table; 6 | public readonly feedbackIndexName: string; 7 | 8 | constructor(scope: Construct, id: string) { 9 | super(scope, id); 10 | 11 | const feedbackIndexName = 'FeedbackIndex'; 12 | const table = new ddb.Table(this, 'Table', { 13 | partitionKey: { 14 | name: 'id', 15 | type: ddb.AttributeType.STRING, 16 | }, 17 | sortKey: { 18 | name: 'createdDate', 19 | type: ddb.AttributeType.STRING, 20 | }, 21 | }); 22 | 23 | table.addGlobalSecondaryIndex({ 24 | indexName: feedbackIndexName, 25 | partitionKey: { 26 | name: 'feedback', 27 | type: ddb.AttributeType.STRING, 28 | }, 29 | }); 30 | 31 | this.table = table; 32 | this.feedbackIndexName = feedbackIndexName; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './auth'; 3 | export * from './web'; 4 | export * from './database'; 5 | export * from './rag'; 6 | export * from './llm'; 7 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/llm.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as sagemaker from '@aws-cdk/aws-sagemaker-alpha'; 3 | import { Model } from 'generative-ai-use-cases-jp'; 4 | 5 | const models: Model[] = [ 6 | { 7 | name: 'llm-jp-13b-instruct-full-jaster-dolly-oasst-v1', 8 | path: 'models/llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0.tar.gz', 9 | prompt_template_name: 'llmJp', 10 | }, 11 | { 12 | name: 'llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1', 13 | path: 'models/llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0.tar.gz', 14 | prompt_template_name: 'llmJp', 15 | }, 16 | ]; 17 | 18 | export class LLM extends Construct { 19 | public readonly models: Model[] = models; 20 | public readonly deploy_suffix: string = 21 | '-' + new Date().toISOString().replace(/[:T-]/g, '').split('.')[0]; 22 | public readonly endpointConfigName; 23 | public readonly endpointName; 24 | 25 | constructor(scope: Construct, id: string) { 26 | super(scope, id); 27 | 28 | // Specify Endpoint with stage suffix 29 | const stage = this.node.tryGetContext('stage'); 30 | const prefix = stage ? `${stage}-` : ''; 31 | this.endpointConfigName = prefix + 'llm-jp-endpoint-config' + this.deploy_suffix; 32 | this.endpointName = prefix + 'llm-jp-endpoint'; 33 | 34 | // Get Container Image 35 | // https://github.com/aws/deep-learning-containers/blob/master/available_images.md 36 | const repositoryName = 'djl-inference'; 37 | const tag = '0.24.0-deepspeed0.10.0-cu118'; 38 | const image = sagemaker.ContainerImage.fromDlc(repositoryName, tag); 39 | 40 | // Create Models 41 | const sm_models = models.map((model) => { 42 | const modelData = sagemaker.ModelData.fromAsset(model.path); 43 | const sm_model = new sagemaker.Model( 44 | this, 45 | `sagemaker-model-${model.name}`, 46 | { 47 | modelName: model.name + this.deploy_suffix, 48 | containers: [ 49 | { 50 | image: image, 51 | modelData: modelData, 52 | }, 53 | ], 54 | } 55 | ); 56 | return sm_model; 57 | }); 58 | 59 | // Create Endpoint Config 60 | const endpointConfig = new sagemaker.EndpointConfig( 61 | this, 62 | 'EndpointConfig', 63 | { 64 | endpointConfigName: this.endpointConfigName, 65 | instanceProductionVariants: models.map((modelConfig, idx) => { 66 | return { 67 | model: sm_models[idx], 68 | variantName: modelConfig.name, 69 | initialVariantWeight: 1, 70 | initialInstanceCount: 1, 71 | instanceType: sagemaker.InstanceType.G5_2XLARGE, 72 | }; 73 | }), 74 | } 75 | ); 76 | sm_models.forEach((sm_model) => 77 | endpointConfig.node.addDependency(sm_model) 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 { Construct } from 'constructs'; 4 | import { UserPool } from 'aws-cdk-lib/aws-cognito'; 5 | import { Duration, Token } from 'aws-cdk-lib'; 6 | import { 7 | AuthorizationType, 8 | CognitoUserPoolsAuthorizer, 9 | LambdaIntegration, 10 | RestApi, 11 | } from 'aws-cdk-lib/aws-apigateway'; 12 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 13 | import { Runtime } from 'aws-cdk-lib/aws-lambda'; 14 | 15 | export interface RagProps { 16 | userPool: UserPool; 17 | api: RestApi; 18 | } 19 | 20 | /** 21 | * RAG を実行するためのリソースを作成する 22 | */ 23 | export class Rag extends Construct { 24 | constructor(scope: Construct, id: string, props: RagProps) { 25 | super(scope, id); 26 | 27 | // Kendra のリソースを作成 28 | // Index 用の IAM Role を作成 29 | const indexRole = new iam.Role(this, 'KendraIndexRole', { 30 | assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'), 31 | }); 32 | 33 | indexRole.addToPolicy( 34 | new iam.PolicyStatement({ 35 | effect: iam.Effect.ALLOW, 36 | resources: ['*'], 37 | actions: ['s3:GetObject'], 38 | }) 39 | ); 40 | 41 | indexRole.addManagedPolicy( 42 | iam.ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLogsFullAccess') 43 | ); 44 | 45 | const index = new kendra.CfnIndex(this, 'KendraIndex', { 46 | name: 'generative-ai-use-cases-index', 47 | edition: 'DEVELOPER_EDITION', 48 | roleArn: indexRole.roleArn, 49 | 50 | // トークンベースのアクセス制御を実施 51 | // 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kendra-index.html#cfn-kendra-index-usercontextpolicy 52 | userContextPolicy: 'USER_TOKEN', 53 | 54 | // 認可に利用する Cognito の情報を設定 55 | userTokenConfigurations: [ 56 | { 57 | jwtTokenTypeConfiguration: { 58 | keyLocation: 'URL', 59 | userNameAttributeField: 'cognito:username', 60 | groupAttributeField: 'cognito:groups', 61 | url: `${props.userPool.userPoolProviderUrl}/.well-known/jwks.json`, 62 | }, 63 | }, 64 | ], 65 | }); 66 | 67 | // WebCrawler を作成 68 | const webCrawlerRole = new iam.Role(this, 'KendraWebCrawlerRole', { 69 | assumedBy: new iam.ServicePrincipal('kendra.amazonaws.com'), 70 | }); 71 | webCrawlerRole.addToPolicy( 72 | new iam.PolicyStatement({ 73 | effect: iam.Effect.ALLOW, 74 | resources: [Token.asString(index.getAtt('Arn'))], 75 | actions: ['kendra:BatchPutDocument', 'kendra:BatchDeleteDocument'], 76 | }) 77 | ); 78 | 79 | new kendra.CfnDataSource(this, 'WebCrawler', { 80 | indexId: index.attrId, 81 | name: 'WebCrawler', 82 | type: 'WEBCRAWLER', 83 | roleArn: webCrawlerRole.roleArn, 84 | languageCode: 'ja', 85 | dataSourceConfiguration: { 86 | webCrawlerConfiguration: { 87 | urls: { 88 | seedUrlConfiguration: { 89 | webCrawlerMode: 'HOST_ONLY', 90 | // デモ用に AWS の GenAI 関連のページを取り込む 91 | seedUrls: [ 92 | 'https://aws.amazon.com/jp/what-is/generative-ai/', 93 | 'https://aws.amazon.com/jp/generative-ai/', 94 | 'https://aws.amazon.com/jp/generative-ai/use-cases/', 95 | 'https://aws.amazon.com/jp/bedrock/', 96 | 'https://aws.amazon.com/jp/bedrock/features/', 97 | 'https://aws.amazon.com/jp/bedrock/testimonials/', 98 | ], 99 | }, 100 | }, 101 | crawlDepth: 1, 102 | urlInclusionPatterns: ['https://aws.amazon.com/jp/.*'], 103 | }, 104 | }, 105 | }); 106 | 107 | // RAG 関連の API を追加する 108 | // Lambda 109 | const queryFunction = new NodejsFunction(this, 'Query', { 110 | runtime: Runtime.NODEJS_18_X, 111 | entry: './lambda/queryKendra.ts', 112 | timeout: Duration.minutes(15), 113 | bundling: { 114 | // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする 115 | externalModules: [], 116 | }, 117 | environment: { 118 | INDEX_ID: index.ref, 119 | }, 120 | }); 121 | queryFunction.role?.addToPrincipalPolicy( 122 | new iam.PolicyStatement({ 123 | effect: iam.Effect.ALLOW, 124 | resources: [Token.asString(index.getAtt('Arn'))], 125 | actions: ['kendra:Query'], 126 | }) 127 | ); 128 | 129 | const retrieveFunction = new NodejsFunction(this, 'Retrieve', { 130 | runtime: Runtime.NODEJS_18_X, 131 | entry: './lambda/retrieveKendra.ts', 132 | timeout: Duration.minutes(15), 133 | bundling: { 134 | // 新しい Kendra の機能を使うため、AWS SDK を明示的にバンドルする 135 | externalModules: [], 136 | }, 137 | environment: { 138 | INDEX_ID: index.ref, 139 | }, 140 | }); 141 | retrieveFunction.role?.addToPrincipalPolicy( 142 | new iam.PolicyStatement({ 143 | effect: iam.Effect.ALLOW, 144 | resources: [Token.asString(index.getAtt('Arn'))], 145 | actions: ['kendra:Retrieve'], 146 | }) 147 | ); 148 | 149 | // API Gateway 150 | const authorizer = new CognitoUserPoolsAuthorizer(this, 'Authorizer', { 151 | cognitoUserPools: [props.userPool], 152 | }); 153 | 154 | const commonAuthorizerProps = { 155 | authorizationType: AuthorizationType.COGNITO, 156 | authorizer, 157 | }; 158 | const ragResource = props.api.root.addResource('rag'); 159 | 160 | const queryResource = ragResource.addResource('query'); 161 | // POST: /query 162 | queryResource.addMethod( 163 | 'POST', 164 | new LambdaIntegration(queryFunction), 165 | commonAuthorizerProps 166 | ); 167 | 168 | const retrieveResource = ragResource.addResource('retrieve'); 169 | // POST: /retrieve 170 | retrieveResource.addMethod( 171 | 'POST', 172 | new LambdaIntegration(retrieveFunction), 173 | commonAuthorizerProps 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /packages/cdk/lib/construct/web.ts: -------------------------------------------------------------------------------- 1 | import { Stack, RemovalPolicy } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { CloudFrontToS3 } from '@aws-solutions-constructs/aws-cloudfront-s3'; 4 | import { Distribution } from 'aws-cdk-lib/aws-cloudfront'; 5 | import { NodejsBuild } from 'deploy-time-build'; 6 | import * as s3 from 'aws-cdk-lib/aws-s3'; 7 | 8 | export interface WebProps { 9 | apiEndpointUrl: string; 10 | userPoolId: string; 11 | userPoolClientId: string; 12 | idPoolId: string; 13 | predictStreamFunctionArn: string; 14 | ragEnabled: boolean; 15 | selfSignUpEnabled: boolean; 16 | endpointName: string; 17 | endpointConfigName: string; 18 | models: string; 19 | } 20 | 21 | export class Web extends Construct { 22 | public readonly distribution: Distribution; 23 | 24 | constructor(scope: Construct, id: string, props: WebProps) { 25 | super(scope, id); 26 | 27 | const { cloudFrontWebDistribution, s3BucketInterface } = new CloudFrontToS3( 28 | this, 29 | 'Web', 30 | { 31 | insertHttpSecurityHeaders: false, 32 | bucketProps: { 33 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 34 | encryption: s3.BucketEncryption.S3_MANAGED, 35 | autoDeleteObjects: true, 36 | removalPolicy: RemovalPolicy.DESTROY, 37 | objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, 38 | enforceSSL: true, 39 | }, 40 | cloudFrontDistributionProps: { 41 | errorResponses: [ 42 | { 43 | httpStatus: 403, 44 | responseHttpStatus: 200, 45 | responsePagePath: '/index.html', 46 | }, 47 | { 48 | httpStatus: 404, 49 | responseHttpStatus: 200, 50 | responsePagePath: '/index.html', 51 | }, 52 | ], 53 | }, 54 | } 55 | ); 56 | 57 | new NodejsBuild(this, 'BuildWeb', { 58 | assets: [ 59 | { 60 | path: '../../', 61 | exclude: [ 62 | '.git', 63 | 'node_modules', 64 | 'packages/cdk/cdk.out', 65 | 'packages/cdk/node_modules', 66 | 'packages/web/dist', 67 | 'packages/web/node_modules', 68 | ], 69 | }, 70 | ], 71 | destinationBucket: s3BucketInterface, 72 | distribution: cloudFrontWebDistribution, 73 | outputSourceDirectory: './packages/web/dist', 74 | buildCommands: ['npm ci', 'npm run web:build'], 75 | buildEnvironment: { 76 | VITE_APP_API_ENDPOINT: props.apiEndpointUrl, 77 | VITE_APP_REGION: Stack.of(this).region, 78 | VITE_APP_USER_POOL_ID: props.userPoolId, 79 | VITE_APP_USER_POOL_CLIENT_ID: props.userPoolClientId, 80 | VITE_APP_IDENTITY_POOL_ID: props.idPoolId, 81 | VITE_APP_PREDICT_STREAM_FUNCTION_ARN: props.predictStreamFunctionArn, 82 | VITE_APP_RAG_ENABLED: props.ragEnabled.toString(), 83 | VITE_APP_SAGEMAKER_ENDPOINT_NAME: props.endpointName, 84 | VITE_APP_SAGEMAKER_ENDPOINT_CONFIG_NAME: props.endpointConfigName, 85 | VITE_APP_SAGEMAKER_MODELS: props.models, 86 | VITE_APP_SELF_SIGN_UP_ENABLED: props.selfSignUpEnabled.toString(), 87 | }, 88 | }); 89 | 90 | this.distribution = cloudFrontWebDistribution; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/cdk/lib/generative-ai-use-cases-stack.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Auth, Api, Web, Database, Rag, LLM } from './construct'; 4 | 5 | export class GenerativeAiUseCasesStack extends Stack { 6 | constructor(scope: Construct, id: string, props?: StackProps) { 7 | super(scope, id, props); 8 | 9 | process.env.overrideWarningsEnabled = 'false'; 10 | 11 | const ragEnabled: boolean = this.node.tryGetContext('ragEnabled') || false; 12 | const selfSignUpEnabled: boolean = 13 | this.node.tryGetContext('selfSignUpEnabled') || false; 14 | 15 | const auth = new Auth(this, 'Auth', { 16 | selfSignUpEnabled, 17 | }); 18 | const database = new Database(this, 'Database'); 19 | const llm = new LLM(this, 'LLM'); 20 | const api = new Api(this, 'API', { 21 | userPool: auth.userPool, 22 | idPool: auth.idPool, 23 | table: database.table, 24 | endpointName: llm.endpointName, 25 | endpointConfigName: llm.endpointConfigName, 26 | }); 27 | 28 | const web = new Web(this, 'Api', { 29 | apiEndpointUrl: api.api.url, 30 | userPoolId: auth.userPool.userPoolId, 31 | userPoolClientId: auth.client.userPoolClientId, 32 | idPoolId: auth.idPool.identityPoolId, 33 | predictStreamFunctionArn: api.predictStreamFunction.functionArn, 34 | ragEnabled, 35 | selfSignUpEnabled, 36 | endpointName: llm.endpointName, 37 | endpointConfigName: llm.endpointConfigName, 38 | models: JSON.stringify(llm.models), 39 | }); 40 | 41 | if (ragEnabled) { 42 | new Rag(this, 'Rag', { 43 | userPool: auth.userPool, 44 | api: api.api, 45 | }); 46 | } 47 | 48 | new CfnOutput(this, 'Region', { 49 | value: this.region, 50 | }); 51 | 52 | new CfnOutput(this, 'WebUrl', { 53 | value: `https://${web.distribution.domainName}`, 54 | }); 55 | 56 | new CfnOutput(this, 'ApiEndpoint', { 57 | value: api.api.url, 58 | }); 59 | 60 | new CfnOutput(this, 'UserPoolId', { value: auth.userPool.userPoolId }); 61 | 62 | new CfnOutput(this, 'UserPoolClientId', { 63 | value: auth.client.userPoolClientId, 64 | }); 65 | 66 | new CfnOutput(this, 'IdPoolId', { value: auth.idPool.identityPoolId }); 67 | 68 | new CfnOutput(this, 'PredictStreamFunctionArn', { 69 | value: api.predictStreamFunction.functionArn, 70 | }); 71 | 72 | new CfnOutput(this, 'RagEnabled', { 73 | value: ragEnabled.toString(), 74 | }); 75 | 76 | new CfnOutput(this, 'SelfSignUpEnabled', { 77 | value: selfSignUpEnabled.toString(), 78 | }); 79 | 80 | new CfnOutput(this, 'SageMakerEndpointName', { 81 | value: JSON.stringify(llm.endpointName), 82 | }); 83 | 84 | new CfnOutput(this, 'SageMakerEndpointConfigName', { 85 | value: JSON.stringify(llm.endpointConfigName), 86 | }); 87 | 88 | new CfnOutput(this, 'SageMakerModels', { 89 | value: JSON.stringify(llm.models), 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/cdk/models/llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/packages/cdk/models/llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0.tar.gz -------------------------------------------------------------------------------- /packages/cdk/models/llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0/serving.properties: -------------------------------------------------------------------------------- 1 | engine=MPI 2 | option.entryPoint=djl_python.huggingface 3 | option.task=text-generation 4 | option.dtype=bf16 5 | option.load_in_8bit=true 6 | option.model_id=tmae/llm-jp-13b-instruct-full-jaster-dolly-oasst-v1.0 7 | option.tensor_parallel_degree=1 8 | option.model_loading_timeout=2400 9 | ooption.low_cpu_mem_usage=true 10 | option.enable_streaming=true -------------------------------------------------------------------------------- /packages/cdk/models/llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/llm-jp/llm-jp-model-playground/d49c2022b64bdb65426bf2dbba82a71d568b3c9c/packages/cdk/models/llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0.tar.gz -------------------------------------------------------------------------------- /packages/cdk/models/llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0/serving.properties: -------------------------------------------------------------------------------- 1 | engine=MPI 2 | option.entryPoint=djl_python.huggingface 3 | option.task=text-generation 4 | option.dtype=bf16 5 | option.load_in_8bit=true 6 | option.model_id=tmae/llm-jp-13b-instruct-lora-jaster-dolly-oasst-v1.0 7 | option.tensor_parallel_degree=1 8 | option.model_loading_timeout=2400 9 | ooption.low_cpu_mem_usage=true 10 | option.enable_streaming=true -------------------------------------------------------------------------------- /packages/cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "private": true, 4 | "version": "0.0.1", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc -w", 8 | "cdk": "cdk", 9 | "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", 10 | "test": "jest" 11 | }, 12 | "devDependencies": { 13 | "@types/aws-lambda": "^8.10.119", 14 | "@types/jest": "^29.5.8", 15 | "@types/node": "^20.4.2", 16 | "@typescript-eslint/eslint-plugin": "^6.5.0", 17 | "@typescript-eslint/parser": "^6.5.0", 18 | "aws-cdk": "2.100.0", 19 | "eslint": "^8.48.0", 20 | "jest": "^29.7.0", 21 | "ts-jest": "^29.1.1", 22 | "ts-node": "^10.9.1", 23 | "typescript": "~5.1.6" 24 | }, 25 | "dependencies": { 26 | "@aws-cdk/aws-cognito-identitypool-alpha": "^2.100.0-alpha.0", 27 | "@aws-cdk/aws-lambda-python-alpha": "^2.100.0-alpha.0", 28 | "@aws-cdk/aws-sagemaker-alpha": "^2.100.0-alpha.0", 29 | "@aws-lambda-powertools/logger": "1.14.2", 30 | "@aws-sdk/client-bedrock-runtime": "^3.422.1", 31 | "@aws-sdk/client-dynamodb": "^3.395.0", 32 | "@aws-sdk/client-kendra": "^3.418.0", 33 | "@aws-sdk/client-sagemaker": "3.410.0", 34 | "@aws-sdk/client-sagemaker-runtime": "3.410.0", 35 | "@aws-sdk/lib-dynamodb": "^3.395.0", 36 | "@aws-solutions-constructs/aws-cloudfront-s3": "^2.41.0", 37 | "aws-cdk-lib": "2.100.0", 38 | "constructs": "^10.0.0", 39 | "deploy-time-build": "^0.3.2", 40 | "source-map-support": "^0.5.21" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/cdk/test/generative-ai-use-cases.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Template } from "aws-cdk-lib/assertions"; 3 | import { GenerativeAiUseCasesStack } from "../lib/generative-ai-use-cases-stack"; 4 | import { test, expect } from "@jest/globals"; 5 | 6 | test("snapshot test", () => { 7 | const app = new cdk.App(); 8 | const stack = new GenerativeAiUseCasesStack(app, "MyTestStack"); 9 | // スタックからテンプレート(JSON)を生成 10 | const template = Template.fromStack(stack).toJSON(); 11 | 12 | // 生成したテンプレートとスナップショットが同じか検証 13 | expect(template).toMatchSnapshot(); 14 | }); -------------------------------------------------------------------------------- /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 | "types": [ 27 | "node" 28 | ] 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "cdk.out" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@types/generative-ai-use-cases-jp", 3 | "types": "src/index.d.ts", 4 | "private": true, 5 | "dependencies": { 6 | "@aws-sdk/client-kendra": "^3.418.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/types/src/base.d.ts: -------------------------------------------------------------------------------- 1 | export type PrimaryKey = { 2 | id: string; 3 | createdDate: string; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/types/src/chat.d.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryKey } from './base'; 2 | 3 | export type Chat = PrimaryKey & { 4 | chatId: string; 5 | usecase: string; 6 | title: string; 7 | updatedDate: string; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/types/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './message'; 3 | export * from './chat'; 4 | export * from './protocol'; 5 | export * from './prompt'; 6 | -------------------------------------------------------------------------------- /packages/types/src/message.d.ts: -------------------------------------------------------------------------------- 1 | import { PrimaryKey } from './base'; 2 | 3 | export type Role = 'system' | 'user' | 'assistant'; 4 | 5 | export type MessageAttributes = { 6 | messageId: string; 7 | usecase: string; 8 | userId: string; 9 | feedback: string; 10 | llmType: string; 11 | }; 12 | 13 | export type UnrecordedMessage = { 14 | role: Role; 15 | content: string; 16 | }; 17 | 18 | export type RecordedMessage = PrimaryKey & 19 | MessageAttributes & 20 | UnrecordedMessage; 21 | 22 | export type ToBeRecordedMessage = UnrecordedMessage & { 23 | messageId: string; 24 | }; 25 | 26 | export type ShownMessage = Partial & 27 | Partial & 28 | UnrecordedMessage; 29 | 30 | export type DocumentComment = { 31 | excerpt: string; 32 | replace?: string; 33 | comment?: string; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/types/src/prompt.d.ts: -------------------------------------------------------------------------------- 1 | export type Model = { 2 | name: string; 3 | path: string; 4 | prompt_template_name: string; 5 | }; 6 | 7 | export type PromptTemplate = { 8 | prefix: string; 9 | suffix: string; 10 | join: string; 11 | user: string; 12 | assistant: string; 13 | system: string; 14 | eos_token: string; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/types/src/protocol.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecordedMessage, 3 | ShownMessage, 4 | ToBeRecordedMessage, 5 | UnrecordedMessage, 6 | } from './message'; 7 | import { Chat } from './chat'; 8 | import { 9 | QueryCommandOutput, 10 | RetrieveCommandOutput, 11 | } from '@aws-sdk/client-kendra'; 12 | 13 | export type CreateChatResponse = { 14 | chat: Chat; 15 | }; 16 | 17 | export type CreateMessagesRequest = { 18 | messages: ToBeRecordedMessage[]; 19 | }; 20 | 21 | export type CreateMessagesResponse = { 22 | messages: RecordedMessage[]; 23 | }; 24 | 25 | export type ListChatsResponse = { 26 | chats: Chat[]; 27 | }; 28 | 29 | export type FindChatByIdResponse = { 30 | chat: Chat; 31 | }; 32 | 33 | export type ListMessagesResponse = { 34 | messages: RecordedMessage[]; 35 | }; 36 | 37 | export type UpdateFeedbackRequest = { 38 | createdDate: string; 39 | feedback: string; 40 | }; 41 | 42 | export type UpdateFeedbackResponse = { 43 | message: RecordedMessage; 44 | }; 45 | 46 | export type UpdateTitleRequest = { 47 | title: string; 48 | }; 49 | 50 | export type UpdateTitleResponse = { 51 | chat: Chat; 52 | }; 53 | 54 | export type PredictParams = { 55 | variant?: string; 56 | max_new_tokens?: number; 57 | temperature?: number; 58 | repetition_penalty?: number; 59 | top_p?: number; 60 | seed?: number; 61 | }; 62 | 63 | export type PredictRequest = { 64 | inputs: string; 65 | params?: PredictParams; 66 | }; 67 | 68 | export type PredictResponse = string; 69 | 70 | export type PredictTitleRequest = { 71 | chat: Chat; 72 | inputs: string; 73 | eos_token: string; 74 | }; 75 | 76 | export type PredictTitleResponse = string; 77 | 78 | export type QueryKendraRequest = { 79 | query: string; 80 | }; 81 | 82 | export type QueryKendraResponse = QueryCommandOutput; 83 | 84 | export type RetrieveKendraRequest = { 85 | query: string; 86 | }; 87 | 88 | export type RetrieveKendraResponse = RetrieveCommandOutput; 89 | 90 | export type CreateEndpointResponse = { 91 | Message: string; 92 | }; 93 | 94 | export type EndpointStatusResponse = { 95 | EndpointStatus: 96 | | 'OutOfService' 97 | | 'Creating' 98 | | 'Updating' 99 | | 'SystemUpdating' 100 | | 'RollingBack' 101 | | 'InService' 102 | | 'Deleting' 103 | | 'Failed' 104 | | 'UpdateRollbackFailed'; 105 | }; 106 | -------------------------------------------------------------------------------- /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 | 'plugin:tailwindcss/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['react-refresh'], 13 | rules: { 14 | 'react-refresh/only-export-components': [ 15 | 'warn', 16 | { allowConstantExport: true }, 17 | ], 18 | // Prettire で実施するので ESLint の Rule は無効化 19 | 'tailwindcss/classnames-order': ['off'], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /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 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | LLM-jp PlayGround 11 | 12 | 13 |
14 | 15 | 16 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@aws-amplify/ui-react": "^5.0.6", 14 | "@aws-sdk/client-cognito-identity": "^3.398.0", 15 | "@aws-sdk/client-kendra": "^3.418.0", 16 | "@aws-sdk/client-lambda": "^3.398.0", 17 | "@aws-sdk/credential-provider-cognito-identity": "^3.398.0", 18 | "@headlessui/react": "^1.7.15", 19 | "@react-icons/all-files": "^4.1.0", 20 | "@types/uuid": "^9.0.2", 21 | "aws-amplify": "^5.3.5", 22 | "axios": "^1.4.0", 23 | "copy-to-clipboard": "^3.3.3", 24 | "immer": "^10.0.2", 25 | "lodash.debounce": "^4.0.8", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-icons": "^4.10.1", 29 | "react-markdown": "^8.0.7", 30 | "react-router-dom": "^6.14.2", 31 | "react-syntax-highlighter": "^15.5.0", 32 | "react-highlight-within-textarea": "^3.2.1", 33 | "remark-breaks": "^3.0.3", 34 | "remark-gfm": "^3.0.1", 35 | "swr": "^2.2.0", 36 | "tailwind-scrollbar": "^3.0.4", 37 | "uuid": "^9.0.0", 38 | "zustand": "^4.3.9" 39 | }, 40 | "devDependencies": { 41 | "@tailwindcss/typography": "^0.5.9", 42 | "@types/react": "^18.2.15", 43 | "@types/react-dom": "^18.2.7", 44 | "@types/react-syntax-highlighter": "^15.5.7", 45 | "@types/lodash.debounce": "^4.0.7", 46 | "@typescript-eslint/eslint-plugin": "^6.0.0", 47 | "@typescript-eslint/parser": "^6.0.0", 48 | "@vitejs/plugin-react": "^4.0.3", 49 | "autoprefixer": "^10.4.14", 50 | "eslint": "^8.48.0", 51 | "eslint-plugin-react-hooks": "^4.6.0", 52 | "eslint-plugin-react-refresh": "^0.4.3", 53 | "eslint-plugin-tailwindcss": "^3.13.0", 54 | "postcss": "^8.4.31", 55 | "prettier": "^3.0.0", 56 | "prettier-plugin-tailwindcss": "^0.4.1", 57 | "tailwindcss": "^3.3.3", 58 | "typescript": "^5.0.2", 59 | "vite": "^4.4.5", 60 | "vite-plugin-svgr": "^3.2.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/web/public/aws.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/web/src/@types/common.d.ts: -------------------------------------------------------------------------------- 1 | export type BaseProps = { 2 | className?: string | undefined; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { 4 | PiDotsThreeVertical, 5 | PiList, 6 | PiSignOut, 7 | PiHouse, 8 | PiToggleLeft, 9 | // PiChatCircleText, 10 | // PiPencil, 11 | // PiNote, 12 | PiChatsCircle, 13 | // PiPenNib, 14 | // PiMagnifyingGlass, 15 | // PiTranslate, 16 | } from 'react-icons/pi'; 17 | import { Outlet } from 'react-router-dom'; 18 | import Drawer, { ItemProps } from './components/Drawer'; 19 | import { Authenticator, translations } from '@aws-amplify/ui-react'; 20 | import { Amplify, I18n } from 'aws-amplify'; 21 | import '@aws-amplify/ui-react/styles.css'; 22 | import MenuDropdown from './components/MenuDropdown'; 23 | import MenuItem from './components/MenuItem'; 24 | import useDrawer from './hooks/useDrawer'; 25 | import useConversation from './hooks/useConversation'; 26 | 27 | // const ragEnabled: boolean = import.meta.env.VITE_APP_RAG_ENABLED === 'true'; 28 | const selfSignUpEnabled: boolean = 29 | import.meta.env.VITE_APP_SELF_SIGN_UP_ENABLED === 'true'; 30 | console.log(selfSignUpEnabled); 31 | 32 | const items: ItemProps[] = [ 33 | { 34 | label: 'ホーム', 35 | to: '/', 36 | icon: , 37 | usecase: true, 38 | }, 39 | { 40 | label: 'Playground (Text)', 41 | to: '/playground-text', 42 | icon: , 43 | usecase: true, 44 | }, 45 | { 46 | label: 'Playground (Chat)', 47 | to: '/playground-chat', 48 | icon: , 49 | usecase: true, 50 | }, 51 | { 52 | label: 'チャット', 53 | to: '/chat', 54 | icon: , 55 | usecase: true, 56 | }, 57 | // ragEnabled 58 | // ? { 59 | // label: 'RAG チャット', 60 | // to: '/rag', 61 | // icon: , 62 | // usecase: true, 63 | // } 64 | // : null, 65 | // { 66 | // label: '文章生成', 67 | // to: '/generate', 68 | // icon: , 69 | // usecase: true, 70 | // }, 71 | // { 72 | // label: '要約', 73 | // to: '/summarize', 74 | // icon: , 75 | // usecase: true, 76 | // }, 77 | // { 78 | // label: '校正', 79 | // to: '/editorial', 80 | // icon: , 81 | // usecase: true, 82 | // }, 83 | // { 84 | // label: '翻訳', 85 | // to: '/translate', 86 | // icon: , 87 | // usecase: true, 88 | // }, 89 | // { 90 | // label: 'Kendra 検索', 91 | // to: '/kendra', 92 | // icon: , 93 | // usecase: false, 94 | // }, 95 | ].flatMap((i) => (i !== null ? [i] : [])); 96 | 97 | // /chat/:chatId の形式から :chatId を返す 98 | // path が別の形式の場合は null を返す 99 | const extractChatId = (path: string): string | null => { 100 | const pattern = /\/chat\/(.+)/; 101 | const match = path.match(pattern); 102 | 103 | return match ? match[1] : null; 104 | }; 105 | 106 | const App: React.FC = () => { 107 | Amplify.configure({ 108 | Auth: { 109 | userPoolId: import.meta.env.VITE_APP_USER_POOL_ID, 110 | userPoolWebClientId: import.meta.env.VITE_APP_USER_POOL_CLIENT_ID, 111 | identityPoolId: import.meta.env.VITE_APP_IDENTITY_POOL_ID, 112 | authenticationFlowType: 'USER_SRP_AUTH', 113 | }, 114 | }); 115 | 116 | I18n.putVocabularies(translations); 117 | I18n.setLanguage('ja'); 118 | 119 | const { switchOpen: switchDrawer } = useDrawer(); 120 | const { pathname } = useLocation(); 121 | const { getConversationTitle } = useConversation(); 122 | 123 | const label = useMemo(() => { 124 | const chatId = extractChatId(pathname); 125 | 126 | if (chatId) { 127 | return getConversationTitle(chatId) || ''; 128 | } else { 129 | return items.find((i) => i.to === pathname)?.label || ''; 130 | } 131 | }, [pathname, getConversationTitle]); 132 | 133 | return ( 134 | ( 138 |
139 | Generative AI on AWS 140 |
141 | ), 142 | }}> 143 | {({ signOut }) => ( 144 |
145 | 146 | 147 |
148 |
149 |
150 | 157 |
158 | 159 | {label} 160 | 161 |
162 | }> 163 | <> 164 | } 166 | onClick={() => { 167 | signOut ? signOut() : null; 168 | }}> 169 | サインアウト 170 | 171 | 172 | 173 |
174 |
175 | 176 |
179 | 180 |
181 |
182 |
183 | )} 184 |
185 | ); 186 | }; 187 | 188 | export default App; 189 | -------------------------------------------------------------------------------- /packages/web/src/assets/aws.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/web/src/assets/model.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon-Resource/Machine-Learning/Res_Amazon-SageMaker_Model_48 4 | 5 | -------------------------------------------------------------------------------- /packages/web/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import { PiInfo, PiXCircle } from 'react-icons/pi'; 4 | 5 | // MEMO: 現在は Error しか実装していない 6 | type Props = BaseProps & { 7 | title?: string; 8 | severity: 'info' | 'error'; 9 | children: React.ReactNode; 10 | }; 11 | 12 | const Alert: React.FC = (props) => { 13 | const colors = useMemo(() => { 14 | if (props.severity === 'error') { 15 | return { 16 | border: 'border-red-500', 17 | bg: 'bg-red-50', 18 | icon: 'text-red-500', 19 | }; 20 | } 21 | return { 22 | border: 'border-sky-500', 23 | bg: 'bg-sky-50', 24 | icon: 'text-sky-500', 25 | }; 26 | }, [props.severity]); 27 | 28 | return ( 29 |
33 |
34 | {props.severity === 'error' && ( 35 | 36 | )} 37 | {props.severity === 'info' && ( 38 | 39 | )} 40 |
41 |
42 |
{props.title}
43 |
{props.children}
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default Alert; 50 | -------------------------------------------------------------------------------- /packages/web/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import { PiSpinnerGap } from 'react-icons/pi'; 4 | 5 | type Props = BaseProps & { 6 | disabled?: boolean; 7 | loading?: boolean; 8 | outlined?: boolean; 9 | onClick: () => void; 10 | children: React.ReactNode; 11 | }; 12 | 13 | const Button: React.FC = (props) => { 14 | return ( 15 | 29 | ); 30 | }; 31 | 32 | export default Button; 33 | -------------------------------------------------------------------------------- /packages/web/src/components/ButtonCopy.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import ButtonIcon from './ButtonIcon'; 3 | import { BaseProps } from '../@types/common'; 4 | import { PiCheck, PiClipboard } from 'react-icons/pi'; 5 | import copy from 'copy-to-clipboard'; 6 | 7 | type Props = BaseProps & { 8 | text: string; 9 | }; 10 | 11 | const ButtonCopy: React.FC = (props) => { 12 | const [showsCheck, setshowsCheck] = useState(false); 13 | 14 | const copyMessage = useCallback((message: string) => { 15 | copy(message); 16 | setshowsCheck(true); 17 | 18 | setTimeout(() => { 19 | setshowsCheck(false); 20 | }, 3000); 21 | }, []); 22 | 23 | return ( 24 | { 27 | copyMessage(props.text); 28 | }}> 29 | {showsCheck ? : } 30 | 31 | ); 32 | }; 33 | 34 | export default ButtonCopy; 35 | -------------------------------------------------------------------------------- /packages/web/src/components/ButtonFeedback.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import ButtonIcon from './ButtonIcon'; 4 | import { 5 | PiThumbsUp, 6 | PiThumbsDown, 7 | PiThumbsUpFill, 8 | PiThumbsDownFill, 9 | } from 'react-icons/pi'; 10 | import { ShownMessage } from 'generative-ai-use-cases-jp'; 11 | 12 | type Props = BaseProps & { 13 | message: ShownMessage; 14 | feedback: string; 15 | onClick: () => void; 16 | disabled: boolean; 17 | }; 18 | 19 | const ButtonFeedback: React.FC = (props) => { 20 | const color = useMemo(() => { 21 | if (props.disabled) { 22 | return 'text-gray-500'; 23 | } 24 | 25 | if (props.feedback === 'good') { 26 | return 'text-green-500'; 27 | } else { 28 | return 'text-red-500'; 29 | } 30 | }, [props]); 31 | 32 | const icon = useMemo(() => { 33 | if (props.feedback === 'good') { 34 | if (props.message.feedback === 'good') { 35 | return ; 36 | } else { 37 | return ; 38 | } 39 | } else { 40 | if (props.message.feedback === 'bad') { 41 | return ; 42 | } else { 43 | return ; 44 | } 45 | } 46 | }, [props]); 47 | 48 | return ( 49 | 53 | {icon} 54 | 55 | ); 56 | }; 57 | 58 | export default ButtonFeedback; 59 | -------------------------------------------------------------------------------- /packages/web/src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RowItem, { RowItemProps } from './RowItem'; 3 | 4 | type Props = RowItemProps & { 5 | label?: string; 6 | hint?: string; 7 | disabled?: boolean; 8 | value: string | number; 9 | options: { 10 | label: string; 11 | value: string | number; 12 | }[]; 13 | onChange: (newValue: string | number) => void; 14 | }; 15 | 16 | const ButtonGroup: React.FC = (props) => { 17 | return ( 18 | 19 | {props.label &&
{props.label}
} 20 |
21 | {props.options.map((option, idx) => ( 22 | 38 | ))} 39 |
40 | {props.hint && ( 41 |
{props.hint}
42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default ButtonGroup; 48 | -------------------------------------------------------------------------------- /packages/web/src/components/ButtonIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | 4 | type Props = BaseProps & { 5 | disabled?: boolean; 6 | onClick: () => void; 7 | children: React.ReactNode; 8 | }; 9 | 10 | const ButtonIcon: React.FC = (props) => { 11 | return ( 12 | 22 | ); 23 | }; 24 | 25 | export default ButtonIcon; 26 | -------------------------------------------------------------------------------- /packages/web/src/components/ButtonSend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PiPaperPlaneRightFill, PiSpinnerGap } from 'react-icons/pi'; 3 | import { BaseProps } from '../@types/common'; 4 | 5 | type Props = BaseProps & { 6 | disabled?: boolean; 7 | loading?: boolean; 8 | onClick: () => void; 9 | }; 10 | 11 | const ButtonSend: React.FC = (props) => { 12 | return ( 13 | 27 | ); 28 | }; 29 | 30 | export default ButtonSend; 31 | -------------------------------------------------------------------------------- /packages/web/src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import RowItem from './RowItem'; 4 | 5 | type Props = BaseProps & { 6 | label?: string; 7 | children: React.ReactNode; 8 | }; 9 | 10 | const Card: React.FC = (props) => { 11 | return ( 12 |
16 | {props.label && ( 17 | 18 | {props.label} 19 | 20 | )} 21 | {props.children} 22 |
23 | ); 24 | }; 25 | 26 | export default Card; 27 | -------------------------------------------------------------------------------- /packages/web/src/components/CardDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import Card from './Card'; 4 | import Button from './Button'; 5 | 6 | type Props = BaseProps & { 7 | label: string; 8 | children: React.ReactNode; 9 | onClickDemo: () => void; 10 | }; 11 | 12 | const CardDemo: React.FC = (props) => { 13 | return ( 14 | 15 |
{props.children}
16 |
17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default CardDemo; 24 | -------------------------------------------------------------------------------- /packages/web/src/components/ChatList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import useConversation from '../hooks/useConversation'; 4 | import { useNavigate, useParams } from 'react-router-dom'; 5 | import ChatListItem from './ChatListItem'; 6 | import { decomposeChatId } from '../utils/ChatUtils'; 7 | 8 | type Props = BaseProps; 9 | 10 | const ChatList: React.FC = (props) => { 11 | const { 12 | conversations, 13 | loading, 14 | deleteConversation, 15 | updateConversationTitle, 16 | } = useConversation(); 17 | const { chatId } = useParams(); 18 | const navigate = useNavigate(); 19 | 20 | const onDelete = useCallback( 21 | (_chatId: string) => { 22 | navigate('/chat'); 23 | return deleteConversation(_chatId).catch(() => { 24 | navigate(`/chat/${_chatId}`); 25 | }); 26 | }, 27 | [deleteConversation, navigate] 28 | ); 29 | 30 | const onUpdateTitle = useCallback( 31 | (_chatId: string, title: string) => { 32 | return updateConversationTitle(_chatId, title); 33 | }, 34 | [updateConversationTitle] 35 | ); 36 | 37 | return ( 38 | <> 39 |
43 | {loading && 44 | new Array(10) 45 | .fill('') 46 | .map((_, idx) => ( 47 |
50 | ))} 51 | {conversations.map((chat) => { 52 | const _chatId = decomposeChatId(chat.chatId); 53 | return ( 54 | 62 | ); 63 | })} 64 |
65 | 66 | ); 67 | }; 68 | 69 | export default ChatList; 70 | -------------------------------------------------------------------------------- /packages/web/src/components/ChatListItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useLayoutEffect, 5 | useMemo, 6 | useRef, 7 | useState, 8 | } from 'react'; 9 | import { BaseProps } from '../@types/common'; 10 | import { Link } from 'react-router-dom'; 11 | import { PiChat, PiCheck, PiPencilLine, PiTrash, PiX } from 'react-icons/pi'; 12 | import ButtonIcon from './ButtonIcon'; 13 | import { Chat } from 'generative-ai-use-cases-jp'; 14 | import { decomposeChatId } from '../utils/ChatUtils'; 15 | import DialogConfirmDeleteChat from './DialogConfirmDeleteChat'; 16 | 17 | type Props = BaseProps & { 18 | active: boolean; 19 | chat: Chat; 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | onDelete: (chatId: string) => Promise; 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | onUpdateTitle: (chatId: string, title: string) => Promise; 24 | }; 25 | 26 | const ChatListItem: React.FC = (props) => { 27 | const [openDialog, setOpenDialog] = useState(false); 28 | const [editing, setEditing] = useState(false); 29 | const chatId = useMemo(() => { 30 | return decomposeChatId(props.chat.chatId) ?? ''; 31 | }, [props.chat.chatId]); 32 | 33 | const inputRef = useRef(null); 34 | const [tempTitle, setTempTitle] = useState(''); 35 | 36 | useEffect(() => { 37 | if (editing) { 38 | setTempTitle(props.chat.title); 39 | } 40 | }, [editing, props.chat.title]); 41 | 42 | const updateTitle = useCallback(() => { 43 | setEditing(false); 44 | props.onUpdateTitle(chatId, tempTitle).catch(() => { 45 | setEditing(true); 46 | }); 47 | }, [chatId, props, tempTitle]); 48 | 49 | useLayoutEffect(() => { 50 | if (editing) { 51 | const listener = (e: DocumentEventMap['keypress']) => { 52 | if (e.key === 'Enter' && !e.shiftKey) { 53 | e.preventDefault(); 54 | 55 | // dispatch 処理の中で Title の更新を行う(同期を取るため) 56 | setTempTitle((newTitle) => { 57 | setEditing(false); 58 | props.onUpdateTitle(chatId, newTitle).catch(() => { 59 | setEditing(true); 60 | }); 61 | return newTitle; 62 | }); 63 | } 64 | }; 65 | inputRef.current?.addEventListener('keypress', listener); 66 | 67 | inputRef.current?.focus(); 68 | 69 | return () => { 70 | // eslint-disable-next-line react-hooks/exhaustive-deps 71 | inputRef.current?.removeEventListener('keypress', listener); 72 | }; 73 | } 74 | // eslint-disable-next-line react-hooks/exhaustive-deps 75 | }, [editing]); 76 | 77 | return ( 78 | <> 79 | {openDialog && ( 80 | { 84 | setOpenDialog(false); 85 | props.onDelete(chatId); 86 | }} 87 | onClose={() => { 88 | setOpenDialog(false); 89 | }} 90 | /> 91 | )} 92 | 98 |
100 |
101 | 102 |
103 |
104 | {editing ? ( 105 | { 111 | setTempTitle(e.target.value); 112 | }} 113 | /> 114 | ) : ( 115 | <>{props.chat.title} 116 | )} 117 | {!editing && ( 118 |
123 | )} 124 |
125 |
126 | {props.active && !editing && ( 127 | <> 128 | { 130 | setEditing(true); 131 | }}> 132 | 133 | 134 | { 136 | setOpenDialog(true); 137 | }}> 138 | 139 | 140 | 141 | )} 142 | {editing && ( 143 | <> 144 | 145 | 146 | 147 | 148 | { 151 | setEditing(false); 152 | }}> 153 | 154 | 155 | 156 | )} 157 |
158 |
159 | 160 | 161 | ); 162 | }; 163 | 164 | export default ChatListItem; 165 | -------------------------------------------------------------------------------- /packages/web/src/components/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import Markdown from './Markdown'; 4 | import ButtonCopy from './ButtonCopy'; 5 | import ButtonFeedback from './ButtonFeedback'; 6 | import { PiUserFill } from 'react-icons/pi'; 7 | import { BaseProps } from '../@types/common'; 8 | import { ShownMessage } from 'generative-ai-use-cases-jp'; 9 | import { ReactComponent as MLLogo } from '../assets/model.svg'; 10 | import useChat from '../hooks/useChat'; 11 | 12 | type Props = BaseProps & { 13 | chatContent?: ShownMessage; 14 | loading?: boolean; 15 | }; 16 | 17 | const ChatMessage: React.FC = (props) => { 18 | const chatContent = useMemo(() => { 19 | return props.chatContent; 20 | }, [props]); 21 | 22 | const { pathname } = useLocation(); 23 | const { sendFeedback } = useChat(pathname); 24 | const [isSendingFeedback, setIsSendingFeedback] = useState(false); 25 | 26 | const disabled = useMemo(() => { 27 | return isSendingFeedback || !props.chatContent?.id; 28 | }, [isSendingFeedback, props]); 29 | 30 | const onSendFeedback = async (feedback: string) => { 31 | if (!disabled) { 32 | setIsSendingFeedback(true); 33 | if (feedback !== chatContent?.feedback) { 34 | await sendFeedback(props.chatContent!.createdDate!, feedback); 35 | } else { 36 | await sendFeedback(props.chatContent!.createdDate!, 'none'); 37 | } 38 | setIsSendingFeedback(false); 39 | } 40 | }; 41 | 42 | return ( 43 |
47 |
51 |
52 | {chatContent?.role === 'user' && ( 53 |
54 | 55 |
56 | )} 57 | {chatContent?.role === 'assistant' && ( 58 |
59 | 60 |
61 | )} 62 | 63 |
64 | {chatContent?.role === 'user' && ( 65 |
66 | {chatContent.content.split('\n').map((c, idx) => ( 67 |
{c}
68 | ))} 69 |
70 | )} 71 | {chatContent?.role === 'assistant' && ( 72 | 73 | {chatContent.content + 74 | `${ 75 | props.loading && (chatContent?.content ?? '') !== '' 76 | ? '▍' 77 | : '' 78 | }`} 79 | 80 | )} 81 | {props.loading && (chatContent?.content ?? '') === '' && ( 82 |
83 | )} 84 |
85 |
86 | 87 |
88 | {chatContent?.role === 'user' &&
} 89 | {chatContent?.role === 'assistant' && !props.loading && ( 90 | <> 91 | 95 | {chatContent && ( 96 | <> 97 | { 103 | onSendFeedback('good'); 104 | }} 105 | /> 106 | onSendFeedback('bad')} 112 | /> 113 | 114 | )} 115 | 116 | )} 117 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default ChatMessage; 124 | -------------------------------------------------------------------------------- /packages/web/src/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RowItem from './RowItem'; 3 | import { BaseProps } from '../@types/common'; 4 | 5 | type Props = BaseProps & { 6 | label: string; 7 | value: boolean; 8 | onChange: (value: boolean) => void; 9 | }; 10 | 11 | const Checkbox: React.FC = (props) => { 12 | return ( 13 | 14 | { 20 | props.onChange(e.target.checked); 21 | }} 22 | /> 23 | 26 | 27 | ); 28 | }; 29 | 30 | export default Checkbox; 31 | -------------------------------------------------------------------------------- /packages/web/src/components/DialogConfirmDeleteChat.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import Button from './Button'; 4 | import ModalDialog from './ModalDialog'; 5 | import { Chat } from 'generative-ai-use-cases-jp'; 6 | import { decomposeChatId } from '../utils/ChatUtils'; 7 | 8 | type Props = BaseProps & { 9 | isOpen: boolean; 10 | target?: Chat; 11 | onDelete: (chatId: string) => void; 12 | onClose: () => void; 13 | }; 14 | 15 | const DialogConfirmDeleteChat: React.FC = (props) => { 16 | return ( 17 | 18 |
19 | チャット 20 | 「{props.target?.title}」 21 | を削除しますか? 22 |
23 | 24 |
25 | 28 | 35 |
36 |
37 | ); 38 | }; 39 | 40 | export default DialogConfirmDeleteChat; 41 | -------------------------------------------------------------------------------- /packages/web/src/components/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import { BaseProps } from '../@types/common'; 3 | import { Link, useLocation } from 'react-router-dom'; 4 | import useDrawer from '../hooks/useDrawer'; 5 | import ButtonIcon from './ButtonIcon'; 6 | import { PiSignOut, PiX } from 'react-icons/pi'; 7 | // import { ReactComponent as MLLogo } from '../assets/model.svg'; 8 | import ChatList from './ChatList'; 9 | import useEndpoint from '../hooks/useEndpoint'; 10 | import Button from './Button'; 11 | 12 | export type ItemProps = BaseProps & { 13 | label: string; 14 | to: string; 15 | icon: JSX.Element; 16 | usecase: boolean; 17 | }; 18 | 19 | const Item: React.FC = (props) => { 20 | const location = useLocation(); 21 | const { switchOpen } = useDrawer(); 22 | 23 | // 狭い画面の場合は、クリックしたらDrawerを閉じる 24 | const onClick = useCallback(() => { 25 | if ( 26 | document 27 | .getElementById('smallDrawerFiller') 28 | ?.classList.contains('visible') 29 | ) { 30 | switchOpen(); 31 | } 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, []); 34 | return ( 35 | 41 | {props.icon} 42 | {props.label} 43 | 44 | ); 45 | }; 46 | 47 | // type RefLinkProps = BaseProps & { 48 | // label: string; 49 | // to: string; 50 | // icon: JSX.Element; 51 | // }; 52 | 53 | // const RefLink: React.FC = (props) => { 54 | // const { switchOpen } = useDrawer(); 55 | 56 | // // 狭い画面の場合は、クリックしたらDrawerを閉じる 57 | // const onClick = useCallback(() => { 58 | // if ( 59 | // document 60 | // .getElementById('smallDrawerFiller') 61 | // ?.classList.contains('visible') 62 | // ) { 63 | // switchOpen(); 64 | // } 65 | // // eslint-disable-next-line react-hooks/exhaustive-deps 66 | // }, []); 67 | 68 | // return ( 69 | // 74 | //
75 | // {props.icon} 76 | //
77 | //
{props.label}
78 | // 79 | // ); 80 | // }; 81 | 82 | type Props = BaseProps & { 83 | signOut: () => void; 84 | items: ItemProps[]; 85 | }; 86 | 87 | const Drawer: React.FC = (props) => { 88 | const { opened, switchOpen } = useDrawer(); 89 | const { status, createEndpoint, deleteEndpoint, fetchEndpoint } = 90 | useEndpoint(); 91 | fetchEndpoint(); 92 | 93 | const usecases = useMemo(() => { 94 | return props.items.filter((i) => i.usecase); 95 | }, [props.items]); 96 | 97 | const tools = useMemo(() => { 98 | return props.items.filter((i) => !i.usecase); 99 | }, [props.items]); 100 | 101 | return ( 102 | <> 103 | 181 | 182 | {opened && ( 183 |
184 | 187 | 188 | 189 |
192 |
193 | )} 194 | 195 | ); 196 | }; 197 | 198 | export default Drawer; 199 | -------------------------------------------------------------------------------- /packages/web/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class ErrorBoundary extends React.Component<{ 4 | children: React.ReactNode; 5 | }> { 6 | componentDidCatch(): void { 7 | this.forceUpdate(); 8 | } 9 | render() { 10 | return <>{this.props.children}; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/web/src/components/ExpandedField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import RowItem, { RowItemProps } from './RowItem'; 3 | import { PiCaretRightFill } from 'react-icons/pi'; 4 | 5 | type Props = RowItemProps & { 6 | label: string; 7 | defaultOpened?: boolean; 8 | optional?: boolean; 9 | children: React.ReactNode; 10 | }; 11 | 12 | const ExpandedField: React.FC = (props) => { 13 | const [expanded, setExpanded] = useState(props.defaultOpened ?? false); 14 | 15 | return ( 16 | 17 |
{ 20 | setExpanded(!expanded); 21 | }}> 22 | 23 | {props.label} 24 | {props.optional && ( 25 | <> 26 | - 27 | Optional 28 | 29 | )} 30 |
31 | 32 | {expanded && <>{props.children}} 33 |
34 | ); 35 | }; 36 | 37 | export default ExpandedField; 38 | -------------------------------------------------------------------------------- /packages/web/src/components/HighlightText.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { TextWithHighlights, Highlight } from '@aws-sdk/client-kendra'; 3 | 4 | type Props = { 5 | textWithHighlights: TextWithHighlights; 6 | }; 7 | 8 | const HighlightText: React.FC = (props) => { 9 | const highlightText = useMemo(() => { 10 | const baseText: string = props.textWithHighlights.Text || ''; 11 | const highlights: Highlight[] = props.textWithHighlights.Highlights || []; 12 | 13 | if (highlights.length === 0) { 14 | return <>{baseText}; 15 | } 16 | 17 | const nodes: React.ReactNode[] = []; 18 | for (let i = 0; i < highlights.length; i++) { 19 | // start 〜 mid がハイライトしない文字列(1つ前の End と Begin の間) 20 | // mid 〜 end がハイライトする文字列(Begin と End の間) 21 | const start = highlights[i - 1]?.EndOffset ?? 0; 22 | const mid = highlights[i]?.BeginOffset ?? baseText.length; 23 | const end = highlights[i]?.EndOffset ?? baseText.length; 24 | 25 | nodes.push( 26 | 27 | {baseText.substring(start, mid)}{' '} 28 | , 29 | 30 | {baseText.substring(mid, end)} 31 | 32 | ); 33 | // すべてのハイライトを処理したら、残りの文字列をまとめて設定 34 | if (i === highlights.length - 1) { 35 | nodes.push( 36 | 37 | {baseText.substring(end)} 38 | 39 | ); 40 | } 41 | } 42 | 43 | return
{nodes}
; 44 | }, [props]); 45 | 46 | return highlightText; 47 | }; 48 | 49 | export default HighlightText; 50 | -------------------------------------------------------------------------------- /packages/web/src/components/InputChatContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from 'react'; 2 | import ButtonSend from './ButtonSend'; 3 | import Textarea from './Textarea'; 4 | import useChat from '../hooks/useChat'; 5 | import { useLocation } from 'react-router-dom'; 6 | import Button from './Button'; 7 | import { PiArrowsCounterClockwise } from 'react-icons/pi'; 8 | 9 | type Props = { 10 | content: string; 11 | disabled?: boolean; 12 | placeholder?: string; 13 | resetDisabled?: boolean; 14 | onChangeContent: (content: string) => void; 15 | onSend: () => void; 16 | onReset: () => void; 17 | }; 18 | 19 | const InputChatContent: React.FC = (props) => { 20 | const { pathname } = useLocation(); 21 | const { loading, isEmpty } = useChat(pathname); 22 | 23 | const disabledSend = useMemo(() => { 24 | return props.content === '' || props.disabled; 25 | }, [props.content, props.disabled]); 26 | 27 | useEffect(() => { 28 | const listener = (e: DocumentEventMap['keypress']) => { 29 | if (e.key === 'Enter' && !e.shiftKey) { 30 | e.preventDefault(); 31 | 32 | if (!disabledSend) { 33 | props.onSend(); 34 | } 35 | } 36 | }; 37 | document 38 | .getElementById('input-chat-content') 39 | ?.addEventListener('keypress', listener); 40 | 41 | return () => { 42 | document 43 | .getElementById('input-chat-content') 44 | ?.removeEventListener('keypress', listener); 45 | }; 46 | }); 47 | 48 | return ( 49 | <> 50 |
53 |