├── .gitignore ├── .npmignore ├── .semgrepignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── solution_architecture.png ├── bin ├── cdk-react-app.d.ts └── cdk-react-app.ts ├── cdk.json ├── jest.config.js ├── lib ├── cdk-react-app-stack.ts ├── constructs │ ├── api-gateway.ts │ ├── lambda.ts │ ├── react-app-build.ts │ ├── react-app-deploy.ts │ └── s3.ts ├── lambda-functions │ ├── get_credentials │ │ └── lambda_handler.py │ └── orchestration │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── connections.py │ │ ├── document_generator.py │ │ ├── exceptions.py │ │ ├── generate.py │ │ ├── prompt_templates.py │ │ ├── requirements.txt │ │ ├── summarization.py │ │ ├── summarize_generate.py │ │ └── utils.py └── react-app │ ├── .env.template │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── worklets │ │ └── audio-processor.js │ ├── src │ ├── App.css │ ├── App.d.ts │ ├── App.tsx │ ├── assets │ │ └── favicon.ico │ ├── components │ │ ├── AudioPlayer.tsx │ │ └── TranscribeForm.tsx │ ├── context │ │ ├── AwsCredentialsContext.tsx │ │ └── SystemAudioContext.tsx │ ├── hooks │ │ ├── useAudioProcessing.ts │ │ ├── useAudioRecorder.ts │ │ └── useAudioTranscription.ts │ ├── index.css │ ├── main.tsx │ ├── services │ │ └── documentApi.ts │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.d.ts │ └── vite.config.ts ├── package-lock.json ├── package.json ├── test ├── cdk-react-app.test.d.ts └── cdk-react-app.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | nfs* 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | ### OSX ### 94 | # General 95 | .DS_Store 96 | .AppleDouble 97 | .LSOverride 98 | 99 | # Icon must end with two \r 100 | Icon 101 | 102 | # Thumbnails 103 | ._* 104 | 105 | # Files that might appear in the root of a volume 106 | .DocumentRevisions-V100 107 | .fseventsd 108 | .Spotlight-V100 109 | .TemporaryItems 110 | .Trashes 111 | .VolumeIcon.icns 112 | .com.apple.timemachine.donotpresent 113 | 114 | # Directories potentially created on remote AFP share 115 | .AppleDB 116 | .AppleDesktop 117 | Network Trash Folder 118 | Temporary Items 119 | .apdisk 120 | 121 | ### PyCharm ### 122 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 123 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 124 | 125 | # User-specific stuff 126 | .idea/**/workspace.xml 127 | .idea/**/tasks.xml 128 | .idea/**/usage.statistics.xml 129 | .idea/**/dictionaries 130 | .idea/**/shelf 131 | 132 | # Generated files 133 | .idea/**/contentModel.xml 134 | 135 | # Sensitive or high-churn files 136 | .idea/**/dataSources/ 137 | .idea/**/dataSources.ids 138 | .idea/**/dataSources.local.xml 139 | .idea/**/sqlDataSources.xml 140 | .idea/**/dynamic.xml 141 | .idea/**/uiDesigner.xml 142 | .idea/**/dbnavigator.xml 143 | 144 | # Gradle 145 | .idea/**/gradle.xml 146 | .idea/**/libraries 147 | 148 | # Gradle and Maven with auto-import 149 | # When using Gradle or Maven with auto-import, you should exclude module files, 150 | # since they will be recreated, and may cause churn. Uncomment if using 151 | # auto-import. 152 | .idea/*.xml 153 | .idea/*.iml 154 | .idea 155 | # .idea/modules 156 | # *.iml 157 | # *.ipr 158 | 159 | # CMake 160 | cmake-build-*/ 161 | 162 | # Mongo Explorer plugin 163 | .idea/**/mongoSettings.xml 164 | 165 | # File-based project format 166 | *.iws 167 | 168 | # IntelliJ 169 | out/ 170 | 171 | # mpeltonen/sbt-idea plugin 172 | .idea_modules/ 173 | 174 | # JIRA plugin 175 | atlassian-ide-plugin.xml 176 | 177 | # Cursive Clojure plugin 178 | .idea/replstate.xml 179 | 180 | # Crashlytics plugin (for Android Studio and IntelliJ) 181 | com_crashlytics_export_strings.xml 182 | crashlytics.properties 183 | crashlytics-build.properties 184 | fabric.properties 185 | 186 | # Editor-based Rest Client 187 | .idea/httpRequests 188 | 189 | # Android studio 3.1+ serialized cache file 190 | .idea/caches/build_file_checksums.ser 191 | 192 | ### PyCharm Patch ### 193 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 194 | 195 | # *.iml 196 | # modules.xml 197 | # .idea/misc.xml 198 | # *.ipr 199 | 200 | # Sonarlint plugin 201 | .idea/sonarlint 202 | 203 | ### Python ### 204 | # Byte-compiled / optimized / DLL files 205 | __pycache__/ 206 | *.py[cod] 207 | *$py.class 208 | 209 | # C extensions 210 | *.so 211 | 212 | # Distribution / packaging 213 | .Python 214 | build/ 215 | develop-eggs/ 216 | dist/ 217 | downloads/ 218 | eggs/ 219 | .eggs/ 220 | lib64/ 221 | parts/ 222 | sdist/ 223 | var/ 224 | wheels/ 225 | pip-wheel-metadata/ 226 | share/python-wheels/ 227 | *.egg-info/ 228 | .installed.cfg 229 | *.egg 230 | MANIFEST 231 | 232 | # PyInstaller 233 | # Usually these files are written by a python script from a template 234 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 235 | *.manifest 236 | *.spec 237 | 238 | # Installer logs 239 | pip-log.txt 240 | pip-delete-this-directory.txt 241 | 242 | # Unit test / coverage reports 243 | htmlcov/ 244 | .tox/ 245 | .nox/ 246 | .coverage 247 | .coverage.* 248 | nosetests.xml 249 | coverage.xml 250 | *.cover 251 | .hypothesis/ 252 | .pytest_cache/ 253 | 254 | # Translations 255 | *.mo 256 | *.pot 257 | 258 | # Django stuff: 259 | local_settings.py 260 | db.sqlite3 261 | db.sqlite3-journal 262 | 263 | # Flask stuff: 264 | instance/ 265 | .webassets-cache 266 | 267 | # Scrapy stuff: 268 | .scrapy 269 | 270 | # Sphinx documentation 271 | docs/_build/ 272 | 273 | # PyBuilder 274 | target/ 275 | 276 | # Jupyter Notebook 277 | .ipynb_checkpoints 278 | 279 | # IPython 280 | profile_default/ 281 | ipython_config.py 282 | 283 | # pyenv 284 | .python-version 285 | 286 | # pipenv 287 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 288 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 289 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 290 | # install all needed dependencies. 291 | #Pipfile.lock 292 | 293 | # celery beat schedule file 294 | celerybeat-schedule 295 | 296 | # SageMath parsed files 297 | *.sage.py 298 | 299 | # Environments 300 | .venv 301 | env/ 302 | venv/ 303 | ENV/ 304 | env.bak/ 305 | venv.bak/ 306 | 307 | # Spyder project settings 308 | .spyderproject 309 | .spyproject 310 | 311 | # Rope project settings 312 | .ropeproject 313 | 314 | # mkdocs documentation 315 | /site 316 | 317 | # mypy 318 | .mypy_cache/ 319 | .dmypy.json 320 | dmypy.json 321 | 322 | # Pyre type checker 323 | .pyre/ 324 | 325 | ### VisualStudioCode ### 326 | .vscode 327 | 328 | ### VisualStudioCode Patch ### 329 | # Ignore all local history of files 330 | .history 331 | 332 | ### Windows ### 333 | # Windows thumbnail cache files 334 | Thumbs.db 335 | Thumbs.db:encryptable 336 | ehthumbs.db 337 | ehthumbs_vista.db 338 | 339 | # Dump file 340 | *.stackdump 341 | 342 | # Folder config file 343 | [Dd]esktop.ini 344 | 345 | # Recycle Bin used on file shares 346 | $RECYCLE.BIN/ 347 | 348 | # Windows Installer files 349 | *.cab 350 | *.msi 351 | *.msix 352 | *.msm 353 | *.msp 354 | 355 | # Windows shortcuts 356 | *.lnk 357 | 358 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode,node 359 | 360 | ### CDK-specific ignores ### 361 | *.swp 362 | cdk.context.json 363 | yarn.lock 364 | .cdk.staging 365 | cdk.out 366 | 367 | !jest.config.js 368 | node_modules 369 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | -------------------------------------------------------------------------------- /.semgrepignore: -------------------------------------------------------------------------------- 1 | cdk.json 2 | lib/react-app/package.json 3 | package.json 4 | 5 | -------------------------------------------------------------------------------- /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 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /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 | # Knowledge Capture using Live Transcribe and GenerativeAI 2 | 3 | Welcome to the Knowledge Capture Solution using Live Transcribe and GenerativeAI! This project provides a comprehensive system for real-time voice transcription, text analysis, anomaly detection, and professional document summarization. Below is an overview of the main features and functionalities of the solution: 4 | 5 | ## Features 6 | 7 | ### 1. Real-Time Voice Transcription 8 | 9 | Our solution offers real-time voice transcription that allows users to review and edit transcriptions to ensure accuracy. The system ensures that user inputs pertain to a single topic, enhancing the quality and relevance of the transcriptions. 10 | 11 | - **Real-Time Transcription:** Transcribe voice inputs in real-time. 12 | - **User Review and Edit:** Users can review and edit the transcriptions to ensure accuracy. 13 | - **Single Topic Focus:** User inputs are expected to focus on a single topic to maintain coherence. 14 | 15 | ### 2. Professional Document Summarization 16 | 17 | Once the transcribed texts are edited and confirmed to be on the same topic, the solution summarizes the set of texts into a professional document ready for review. 18 | 19 | - **Text Summarization:** Summarize edited and validated transcriptions into a coherent document. 20 | - **Professional Quality:** Ensure the document is professionally formatted and ready for review. 21 | 22 | ## Getting Started 23 | 24 | To get started with this project, clone the repository and follow the installation instructions in the README file. The solution is designed to be user-friendly and integrates seamlessly with your existing workflows. 25 | 26 | We hope this project helps streamline your voice transcription and document creation processes, ensuring accuracy and efficiency every step of the way. 27 | 28 | ## Table of Contents 29 | 30 | - [Knowledge Capture using Live Transcribe and GenerativeAI](#knowledge-capture-using-live-transcribe-and-generativeai) 31 | - [Features](#features) 32 | - [1. Real-Time Voice Transcription](#1-real-time-voice-transcription) 33 | - [2. Professional Document Summarization](#2-professional-document-summarization) 34 | - [Getting Started](#getting-started) 35 | - [Table of Contents](#table-of-contents) 36 | - [Prerequisites](#prerequisites) 37 | - [Target technology stack](#target-technology-stack) 38 | - [Solution Overview](#solution-overview) 39 | - [Solution Architecture](#solution-architecture) 40 | - [Deployment](#deployment) 41 | - [Useful CDK commands](#useful-cdk-commands) 42 | - [Authors and acknowledgment](#authors-and-acknowledgment) 43 | 44 | ## Prerequisites 45 | 46 | - Docker 47 | - AWS CDK Toolkit 2.114.1+, installed installed and configured. For more information, see Getting started with the AWS CDK in the AWS CDK documentation. 48 | - Python 3.12+, installed and configured. For more information, see Beginners Guide/Download in the Python documentation. 49 | - An active AWS account 50 | - An AWS account bootstrapped by using AWS CDK in us-east-1 or us-west-2. Enable Claude model access in Bedrock service. 51 | - An AWS IAM user/role with access to Amazon Transcribe, Amazon Bedrock, Amazon S3, and Amazon Lambda 52 | 53 | ## Target technology stack 54 | 55 | - Amazon Bedrock 56 | - Amazon Lambda 57 | - Amazon S3 58 | - Amazon Transcribe Live 59 | - Amazon CloudFront 60 | - Amazon API Gateway 61 | - AWS ColdBuild 62 | - AWS CDK 63 | - AWS EventBridge 64 | - AWS IAM 65 | - AWS Key Management Service 66 | - AWS Parameter Store 67 | - React 68 | 69 | ## Solution Overview 70 | 71 | ## Solution Architecture 72 | 73 | ![Architecture Diagram](assets/solution_architecture.png) 74 | 75 | This diagram outlines a workflow for a voice-based application using AWS services. Here’s a step-by-step description of the workflow: 76 | 77 | 1. **User Interaction**: 78 | 79 | - A user interacts with the system through a UI on a device. This UI has a voice input feature. 80 | 81 | 2. **API Gateway**: 82 | 83 | - The voice input from the user is sent to the API Gateway. The API Gateway acts as an entry point for all the API calls, handling request routing, authorization, and throttling. 84 | 85 | 3. **Authorization Lambda Function**: 86 | 87 | - The API Gateway triggers an AWS Lambda function for authorization. This function verifies if the user has the necessary permissions to access the services. 88 | 89 | 4. **Amazon Transcribe Live**: 90 | 91 | - Once authorized, the voice input is processed by Amazon Transcribe Live, which converts the speech to text in real-time. 92 | 93 | 5. **Orchestration Lambda Function**: 94 | 95 | - The transcribed text is then sent to another AWS Lambda function responsible for summarizing and generating content and storing the recorded audio files in S3. This could involve processing the text for various purposes, such as creating summaries, generating responses, or performing further analysis. 96 | 97 | 6. **Amazon S3 Bucket**: 98 | 99 | - Any generated content, summaries, or processed data are stored in an Amazon S3 bucket. This allows for scalable and durable storage of the processed information. 100 | 101 | 7. **Return to UI**: 102 | - Finally, the processed information or responses are sent back to the user’s device through the API Gateway, completing the workflow cycle. 103 | 104 | ## Deployment 105 | 106 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 107 | 108 | This project is set up like a standard Python project. The initialization 109 | process also creates a virtualenv within this project, stored under the `.venv` 110 | directory. To create the virtualenv it assumes that there is a `python3` 111 | (or `python` for Windows) executable in your path with access to the `venv` 112 | package. If for any reason the automatic creation of the virtualenv fails, 113 | you can create the virtualenv manually. 114 | 115 | To manually create a virtualenv on MacOS and Linux: 116 | 117 | ```bash 118 | $ python3 -m venv .venv 119 | ``` 120 | 121 | After the init process completes and the virtualenv is created, you can use the following 122 | step to activate your virtualenv. 123 | 124 | ```bash 125 | $ source .venv/bin/activate 126 | ``` 127 | 128 | If you are a Windows platform, you would activate the virtualenv like this: 129 | 130 | ```powershell 131 | % .venv\Scripts\activate.bat 132 | ``` 133 | 134 | Once the virtualenv is activated, you can install the required dependencies. 135 | 136 | ```bash 137 | $ npm install 138 | ``` 139 | 140 | Once dependencies are installed, you can proceed to deploy cdk. 141 | 142 | ``` 143 | $ cdk deploy 144 | ``` 145 | 146 | If this is your first time deploying it, the process may take approximately 30-45 minutes to build several Docker images in ECS (Amazon Elastic Container Service). Please be patient until it's completed. Afterward, it will start deploying the docgen-stack, which typically takes about 5-8 minutes. 147 | 148 | Once the deployment process is complete, you will see the output of the cdk in the terminal, and you can also verify the status in your CloudFormation console. 149 | 150 | To delete the cdk once you have finished using it to avoid future costs, you can either delete it through the console or execute the following command in the terminal. 151 | 152 | ```bash 153 | $ cdk destroy 154 | ``` 155 | 156 | You may also need to manually delete the S3 bucket generated by the cdk. Please ensure to delete all the generated resources to avoid incurring costs. 157 | 158 | ## Useful CDK commands 159 | 160 | - `cdk ls` list all stacks in the app 161 | - `cdk synth` emits the synthesized CloudFormation template 162 | - `cdk deploy` deploy this stack to your default AWS account/region 163 | - `cdk diff` compare deployed stack with current state 164 | - `cdk docs` open CDK documentation 165 | - `cdk destroy` dstroys one or more specified stacks 166 | 167 | ## Authors and acknowledgment 168 | 169 | Jundong Qiao (jdqiao@amazon.com) 170 | Praveen Kumar Jeyarajan (pjeyaraj@amazon.com) 171 | Michael Massey (mmssym@amazon.com) 172 | -------------------------------------------------------------------------------- /assets/solution_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/genai-knowledge-capture-webapp/2707ab1c8bcdc39304c2229b579319ec78537788/assets/solution_architecture.png -------------------------------------------------------------------------------- /bin/cdk-react-app.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | -------------------------------------------------------------------------------- /bin/cdk-react-app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { CdkReactAppStack } from '../lib/cdk-react-app-stack'; 5 | 6 | const app = new cdk.App(); 7 | new CdkReactAppStack(app, 'CdkReactAppStack', { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | 12 | /* Uncomment the next line to specialize this stack for the AWS Account 13 | * and Region that are implied by the current CLI configuration. */ 14 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 15 | 16 | /* Uncomment the next line if you know exactly what Account and Region you 17 | * want to deploy the stack to. */ 18 | // env: { account: '', region: '' }, 19 | 20 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 21 | }); -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk-react-app.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 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 59 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 60 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 61 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 62 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/cdk-react-app-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { S3Buckets } from "./constructs/s3"; 3 | import { LambdaFunctions } from "./constructs/lambda"; 4 | import { ApiGateway } from "./constructs/api-gateway"; 5 | import { ReactAppBuild } from "./constructs/react-app-build"; 6 | import { ReactAppDeploy } from "./constructs/react-app-deploy"; 7 | import { AwsSolutionsChecks } from "cdk-nag"; 8 | import { Key } from "aws-cdk-lib/aws-kms"; 9 | import { Aspects, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; 10 | import { NagSuppressions } from "cdk-nag"; 11 | 12 | export class CdkReactAppStack extends Stack { 13 | constructor(scope: Construct, id: string, props?: StackProps) { 14 | super(scope, id, props); 15 | 16 | // Apply AwsSolutionsChecks 17 | Aspects.of(this).add(new AwsSolutionsChecks({ verbose: true })); 18 | 19 | // Create the KMS key 20 | const kmsKey = new Key(this, "SSEKmsKey", { 21 | alias: "transcribe-kms-key", 22 | description: "Customer-managed KMS key for SSE-KMS encryption", 23 | removalPolicy: RemovalPolicy.DESTROY, 24 | enableKeyRotation: true, 25 | }); 26 | 27 | // Create the S3 buckets 28 | const s3Buckets = new S3Buckets(this, "S3Buckets"); 29 | 30 | // Create the Lambda functions 31 | const lambdaFunctions = new LambdaFunctions(this, "LambdaFunctions", { 32 | documentBucket: s3Buckets.documentBucket, 33 | }); 34 | 35 | // Create the API Gateway 36 | const api = new ApiGateway(this, "ApiGateway", { 37 | getCredentialsLambdaFunction: 38 | lambdaFunctions.getCredentialsLambdaFunction, 39 | orchestrationFunction: lambdaFunctions.orchestrationFunction, 40 | }); 41 | 42 | // Create the React app build 43 | new ReactAppBuild(this, "ReactAppBuild", { 44 | kmsKey: kmsKey, 45 | reactAppBucket: s3Buckets.reactAppBucket, 46 | apiUrl: api.apiUrl, 47 | apiKeyParameterName: api.apiKeyParameterName, 48 | }); 49 | 50 | // Create the React app hosting 51 | new ReactAppDeploy(this, "ReactAppDeploy", { 52 | kmsKey: kmsKey, 53 | reactAppBucket: s3Buckets.reactAppBucket, 54 | }); 55 | 56 | NagSuppressions.addStackSuppressions(this, [ 57 | { 58 | id: "AwsSolutions-IAM4", 59 | reason: 60 | "Suppressing L3 IAM policies since it is not managed by the application", 61 | }, 62 | { 63 | id: "AwsSolutions-IAM5", 64 | reason: 65 | "Suppressing L3 IAM policies since it is not managed by the application", 66 | }, 67 | { 68 | id: "AwsSolutions-L1", 69 | reason: "Lambda managed by L3 construct", 70 | }, 71 | ]); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/constructs/api-gateway.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, RemovalPolicy, Stack } from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { 4 | AuthorizationType, 5 | CorsOptions, 6 | LambdaIntegration, 7 | LogGroupLogDestination, 8 | MethodLoggingLevel, 9 | AccessLogFormat, 10 | Deployment, 11 | RestApi, 12 | Stage, 13 | Method, 14 | Cors, 15 | } from "aws-cdk-lib/aws-apigateway"; 16 | import { IFunction } from "aws-cdk-lib/aws-lambda"; 17 | import { LogGroup } from "aws-cdk-lib/aws-logs"; 18 | import { CfnWebACL, CfnWebACLAssociation } from "aws-cdk-lib/aws-wafv2"; 19 | import { ParameterTier, StringParameter } from "aws-cdk-lib/aws-ssm"; 20 | import { NagSuppressions } from "cdk-nag"; 21 | 22 | interface ApiGatewayProps { 23 | getCredentialsLambdaFunction: IFunction; 24 | orchestrationFunction: IFunction; 25 | } 26 | 27 | export class ApiGateway extends Construct { 28 | public readonly apiUrl: string; 29 | public readonly apiKeyParameterName: string; 30 | 31 | constructor(scope: Construct, id: string, props: ApiGatewayProps) { 32 | super(scope, id); 33 | 34 | // Create the REST API 35 | const restApi = this.createRestApi(); 36 | 37 | // Create the WebACL 38 | const restApiACL = this.createWebACL(); 39 | restApiACL.applyRemovalPolicy(RemovalPolicy.DESTROY); 40 | 41 | // Create the API's deployment and stage 42 | const devStage = this.createDeploymentAndStage(restApi); 43 | 44 | // Associate the WebACL with the API stage 45 | this.associateWebACLWithDevStage(devStage, restApiACL); 46 | 47 | // Create the API's "get-credentials" resource and method 48 | const getCredentialsMethod = this.createResourceAndMethod( 49 | restApi, 50 | "get-credentials", 51 | "GET", 52 | props.getCredentialsLambdaFunction 53 | ); 54 | 55 | // Create the API's "orchestration" resource and method 56 | const orchestrationMethod = this.createResourceAndMethod( 57 | restApi, 58 | "orchestration", 59 | "POST", 60 | props.orchestrationFunction 61 | ); 62 | 63 | // Store the API key in Parameter Store and associate it with the stage 64 | const { apiKeyParameter, apiKey } = 65 | this.createApiKeyInParameterStore(devStage); 66 | 67 | // Create an usage plan and associate it with the API key and stage 68 | this.createUsagePlanAndAssociateWithApiKeyAndStage( 69 | restApi, 70 | apiKey, 71 | getCredentialsMethod, 72 | orchestrationMethod 73 | ); 74 | 75 | this.apiUrl = restApi.url; 76 | this.apiKeyParameterName = apiKeyParameter.parameterName; 77 | 78 | // Create CloudFormation outputs for the API 79 | this.createOutputs(); 80 | } 81 | 82 | private createRestApi(): RestApi { 83 | const corsOptions: CorsOptions = { 84 | allowOrigins: ["*"], 85 | allowMethods: ["OPTIONS", "GET", "POST", "PUT", "DELETE"], 86 | allowHeaders: [ 87 | "Content-Type", 88 | "X-Amz-Date", 89 | "Authorization", 90 | "X-Api-Key", 91 | "X-Amz-Security-Token", 92 | "X-Amz-User-Agent", 93 | ], 94 | }; 95 | 96 | const restApi = new RestApi(this, "TranscribeApi", { 97 | restApiName: "Transcribe Service", 98 | description: "This service provides Transcribe capabilities.", 99 | cloudWatchRole: true, 100 | deploy: false, 101 | defaultCorsPreflightOptions: corsOptions, 102 | defaultMethodOptions: { authorizationType: AuthorizationType.NONE }, 103 | }); 104 | NagSuppressions.addResourceSuppressions( 105 | restApi, 106 | [ 107 | { 108 | id: "AwsSolutions-APIG2", 109 | reason: "Request validation implemented within AWS Lambda code", 110 | }, 111 | { 112 | id: "AwsSolutions-APIG4", 113 | reason: "API Key enforced for authorization", 114 | }, 115 | { 116 | id: "AwsSolutions-COG4", 117 | reason: "Cognito pool not required as it is a demo application", 118 | }, 119 | { 120 | id: "AwsSolutions-IAM4", 121 | reason: "Managed policy implemented by RestApi construct", 122 | }, 123 | ], 124 | true 125 | ); 126 | 127 | return restApi; 128 | } 129 | 130 | private createWebACL(): CfnWebACL { 131 | return new CfnWebACL(this, "APIAcl", { 132 | defaultAction: { 133 | allow: {}, 134 | }, 135 | scope: "REGIONAL", 136 | visibilityConfig: { 137 | cloudWatchMetricsEnabled: true, 138 | metricName: "MetricForWebACL", 139 | sampledRequestsEnabled: true, 140 | }, 141 | rules: [ 142 | { 143 | name: "CRSRule", 144 | priority: 0, 145 | statement: { 146 | managedRuleGroupStatement: { 147 | name: "AWSManagedRulesCommonRuleSet", 148 | vendorName: "AWS", 149 | ruleActionOverrides: [ 150 | // Allow API payload greater than 8K 151 | { 152 | actionToUse: { 153 | allow: {}, 154 | }, 155 | name: "SizeRestrictions_BODY", 156 | }, 157 | ], 158 | }, 159 | }, 160 | visibilityConfig: { 161 | cloudWatchMetricsEnabled: true, 162 | metricName: "MetricForWebACLCDK-CRS", 163 | sampledRequestsEnabled: true, 164 | }, 165 | overrideAction: { 166 | none: {}, 167 | }, 168 | }, 169 | ], 170 | }); 171 | } 172 | 173 | private createDeploymentAndStage(restApi: RestApi): Stage { 174 | const devLogGroup = new LogGroup(this, "DevLogs"); 175 | const deployment = new Deployment(this, "Deployment", { api: restApi }); 176 | const devStage = new Stage(this, "dev", { 177 | deployment, 178 | stageName: "dev", 179 | accessLogDestination: new LogGroupLogDestination(devLogGroup), 180 | accessLogFormat: AccessLogFormat.jsonWithStandardFields(), 181 | loggingLevel: MethodLoggingLevel.INFO, 182 | dataTraceEnabled: true, 183 | }); 184 | restApi.deploymentStage = devStage; 185 | 186 | return devStage; 187 | } 188 | 189 | private associateWebACLWithDevStage( 190 | devStage: Stage, 191 | restApiACL: CfnWebACL 192 | ): void { 193 | new CfnWebACLAssociation(this, "DevStageApiACLAssociation", { 194 | resourceArn: devStage.stageArn, 195 | webAclArn: restApiACL.attrArn, 196 | }); 197 | } 198 | 199 | private createResourceAndMethod( 200 | restApi: RestApi, 201 | resourceName: string, 202 | methodType: string, 203 | lambdaFunction: IFunction 204 | ): Method { 205 | const integration = new LambdaIntegration(lambdaFunction); 206 | const resource = restApi.root.addResource(resourceName, { 207 | defaultCorsPreflightOptions: { 208 | allowOrigins: Cors.ALL_ORIGINS, 209 | allowHeaders: Cors.DEFAULT_HEADERS, 210 | allowMethods: Cors.ALL_METHODS, 211 | } 212 | }); 213 | const method = resource.addMethod(methodType, integration, { 214 | apiKeyRequired: true, 215 | }); 216 | NagSuppressions.addResourceSuppressions( 217 | resource, 218 | [ 219 | { 220 | id: "AwsSolutions-APIG4", 221 | reason: "API Key enforced for authorization", 222 | }, 223 | { 224 | id: "AwsSolutions-COG4", 225 | reason: "Cognito pool not required as it is a demo application", 226 | }, 227 | ], 228 | true 229 | ); 230 | 231 | return method; 232 | } 233 | 234 | private createApiKeyInParameterStore(devStage: Stage): { 235 | apiKeyParameter: StringParameter; 236 | apiKey: any; 237 | } { 238 | const apiKeyParameter = new StringParameter(this, "ApiKeyParameter", { 239 | parameterName: "transcribe-api-key", 240 | stringValue: Stack.of(this).node.addr, 241 | description: "The API key for the Transcribe application", 242 | tier: ParameterTier.STANDARD, 243 | }); 244 | 245 | const apiKey = devStage.addApiKey("ApiKey", { 246 | value: apiKeyParameter.stringValue, 247 | }); 248 | 249 | return { apiKeyParameter, apiKey }; 250 | } 251 | 252 | private createUsagePlanAndAssociateWithApiKeyAndStage( 253 | restApi: RestApi, 254 | apiKey: any, 255 | getCredentialsMethod: Method, 256 | orchestrationMethod: Method 257 | ): void { 258 | const usagePlan = restApi.addUsagePlan("UsagePlan", { 259 | name: "Easy", 260 | throttle: { 261 | rateLimit: 100, 262 | burstLimit: 200, 263 | }, 264 | }); 265 | 266 | usagePlan.addApiKey(apiKey); 267 | usagePlan.addApiStage({ 268 | stage: restApi.deploymentStage, 269 | throttle: [ 270 | { 271 | method: getCredentialsMethod, 272 | throttle: { 273 | rateLimit: 100, 274 | burstLimit: 200, 275 | }, 276 | }, 277 | { 278 | method: orchestrationMethod, 279 | throttle: { 280 | rateLimit: 100, 281 | burstLimit: 200, 282 | }, 283 | }, 284 | ], 285 | }); 286 | } 287 | 288 | private createOutputs(): void { 289 | new CfnOutput(this, "ApiUrlOutput", { 290 | key: "ApiUrl", 291 | value: this.apiUrl, 292 | description: "The URL of the API Gateway endpoint", 293 | }); 294 | 295 | new CfnOutput(this, "ApiKeyParameterOutput", { 296 | key: "ApiKeyParameterName", 297 | value: this.apiKeyParameterName, 298 | description: "The API Key parameter in Systems Manager Parameter Store", 299 | }); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /lib/constructs/lambda.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { Aws, Duration } from "aws-cdk-lib"; 3 | import { Construct } from "constructs"; 4 | import { 5 | Alias, 6 | Architecture, 7 | Code, 8 | DockerImageCode, 9 | DockerImageFunction, 10 | Function, 11 | Runtime, 12 | } from "aws-cdk-lib/aws-lambda"; 13 | import { 14 | Effect, 15 | PolicyDocument, 16 | PolicyStatement, 17 | Role, 18 | ServicePrincipal, 19 | } from "aws-cdk-lib/aws-iam"; 20 | import { IBucket } from "aws-cdk-lib/aws-s3"; 21 | import { NagSuppressions } from "cdk-nag"; 22 | 23 | interface LambdaFunctionsProps { 24 | documentBucket: IBucket; 25 | } 26 | 27 | export class LambdaFunctions extends Construct { 28 | public readonly getCredentialsLambdaFunction: Function; 29 | public readonly orchestrationFunction: Function; 30 | 31 | constructor(scope: Construct, id: string, props: LambdaFunctionsProps) { 32 | super(scope, id); 33 | 34 | // Create the Lambda execution role for the get-credentials Lambda function 35 | const getCredentialsLambdaExecutionRole = 36 | this.createGetCredentialsLambdaExecutionRole(); 37 | 38 | // Create the Transcribe role for the credentials that will be 39 | // returned by the getCredentials Lambda function 40 | const transcribeRole = this.createTranscribeRole( 41 | getCredentialsLambdaExecutionRole 42 | ); 43 | 44 | // Create the get-credentials lambda function 45 | this.getCredentialsLambdaFunction = this.createGetCredentialsLambdaFunction( 46 | getCredentialsLambdaExecutionRole, 47 | transcribeRole 48 | ); 49 | 50 | // Create the Lambda execution role for the orchestration Lambda function 51 | const orchestrationLambdaExecutionRole = 52 | this.createOrchestrationLambdaExecutionRole(props.documentBucket); 53 | 54 | // Create the orchestration Lambda function 55 | this.orchestrationFunction = 56 | this.createOrchestrationLambdaFunction( 57 | orchestrationLambdaExecutionRole, 58 | props.documentBucket 59 | ); 60 | } 61 | 62 | private createGetCredentialsLambdaExecutionRole(): Role { 63 | const role = new Role(this, "GetCredentialsLambdaExecutionRole", { 64 | roleName: "transcribe-get-credentials-lambda-role", 65 | assumedBy: new ServicePrincipal("lambda.amazonaws.com"), 66 | inlinePolicies: { 67 | LambdaExecutionPolicy: new PolicyDocument({ 68 | statements: [ 69 | new PolicyStatement({ 70 | effect: Effect.ALLOW, 71 | actions: [ 72 | "logs:CreateLogGroup", 73 | "logs:CreateLogStream", 74 | "logs:PutLogEvents", 75 | ], 76 | resources: ["arn:aws:logs:*:*:*"], 77 | }), 78 | ], 79 | }), 80 | }, 81 | }); 82 | NagSuppressions.addResourceSuppressions(role, [ 83 | { 84 | id: "AwsSolutions-IAM5", 85 | reason: "Wild card resource used for creating log group and log stream", 86 | }, 87 | ]); 88 | role.addToPolicy( 89 | new PolicyStatement({ 90 | actions: ["sts:AssumeRole"], 91 | resources: [role.roleArn], 92 | }) 93 | ); 94 | 95 | return role; 96 | } 97 | 98 | private createTranscribeRole(getCredentialsLambdaExecutionRole: Role): Role { 99 | const role = new Role(this, "TranscribeRole", { 100 | roleName: "transcribe-credentials-role", 101 | assumedBy: getCredentialsLambdaExecutionRole.grantPrincipal, 102 | inlinePolicies: { 103 | TranscribePolicy: new PolicyDocument({ 104 | statements: [ 105 | new PolicyStatement({ 106 | effect: Effect.ALLOW, 107 | actions: [ 108 | "transcribe:StartStreamTranscription", 109 | "transcribe:StartStreamTranscriptionWebSocket", 110 | "transcribe:GetTranscriptionJob", 111 | "transcribe:ListTranscriptionJobs", 112 | "transcribe:GetTranscriptionResult", 113 | ], 114 | resources: ["*"], 115 | }), 116 | ], 117 | }), 118 | }, 119 | }); 120 | NagSuppressions.addResourceSuppressions(role, [ 121 | { 122 | id: "AwsSolutions-IAM5", 123 | reason: "Wild card used for AWS Transcribe service", 124 | }, 125 | ]); 126 | 127 | return role; 128 | } 129 | 130 | private createGetCredentialsLambdaFunction( 131 | getCredentialsLambdaExecutionRole: Role, 132 | transcribeRole: Role 133 | ): Function { 134 | return new Function(this, "GetCredentialsLambdaFunction", { 135 | functionName: "transcribe-get-credentials-function", 136 | runtime: Runtime.PYTHON_3_12, 137 | code: Code.fromAsset( 138 | path.join(__dirname, "../lambda-functions/get_credentials") 139 | ), 140 | handler: "lambda_handler.lambda_handler", 141 | role: getCredentialsLambdaExecutionRole, 142 | timeout: Duration.minutes(1), 143 | memorySize: 128, 144 | environment: { 145 | ROLE_ARN: transcribeRole.roleArn, 146 | }, 147 | }); 148 | } 149 | 150 | private createOrchestrationLambdaExecutionRole( 151 | documentBucket: IBucket 152 | ): Role { 153 | const role = new Role(this, "OrchestrationLambdaExecutionRole", { 154 | roleName: "transcribe-orchestration-lambda-role", 155 | assumedBy: new ServicePrincipal("lambda.amazonaws.com"), 156 | inlinePolicies: { 157 | LambdaExecutionPolicy: new PolicyDocument({ 158 | statements: [ 159 | new PolicyStatement({ 160 | effect: Effect.ALLOW, 161 | actions: [ 162 | "logs:CreateLogGroup", 163 | "logs:CreateLogStream", 164 | "logs:PutLogEvents", 165 | ], 166 | resources: ["arn:aws:logs:*:*:*"], 167 | }), 168 | new PolicyStatement({ 169 | effect: Effect.ALLOW, 170 | actions: [ 171 | "s3:GeneratePresignedUrl", 172 | "s3:GetObject", 173 | "s3:PutObject", 174 | ], 175 | resources: [ 176 | documentBucket.bucketArn, 177 | `${documentBucket.bucketArn}/*`, 178 | ], 179 | }), 180 | new PolicyStatement({ 181 | effect: Effect.ALLOW, 182 | actions: ["bedrock:*"], 183 | resources: [`arn:aws:bedrock:${Aws.REGION}::foundation-model/*`], 184 | }), 185 | ], 186 | }), 187 | }, 188 | }); 189 | NagSuppressions.addResourceSuppressions(role, [ 190 | { 191 | id: "AwsSolutions-IAM5", 192 | reason: 193 | "Wild card resources used for creating log group and log stream and S3 bucket path.", 194 | }, 195 | ]); 196 | 197 | return role; 198 | } 199 | 200 | private createOrchestrationLambdaFunction( 201 | orchestrationLambdaExecutionRole: Role, 202 | documentBucket: IBucket 203 | ): DockerImageFunction { 204 | const orchestrationFunction = new DockerImageFunction(this, "OrchestrationFunction", { 205 | functionName: "transcribe-orchestration-function", 206 | code: DockerImageCode.fromImageAsset( 207 | path.join(__dirname, "../lambda-functions/orchestration") 208 | ), 209 | role: orchestrationLambdaExecutionRole, 210 | architecture: Architecture.ARM_64, 211 | timeout: Duration.seconds(30), 212 | memorySize: 1024, 213 | environment: { 214 | S3_BUCKET_NAME: documentBucket.bucketName, 215 | POWERTOOLS_LOG_LEVEL: "DEBUG", 216 | POWERTOOLS_METRICS_NAMESPACE: "knowledge-capture-dev", 217 | POWERTOOLS_SERVICE_NAME: "knowledge-capture-dev", 218 | XDG_CACHE_HOME: "/tmp", // Required for PDF font cache 219 | }, 220 | }); 221 | new Alias(this, 'LambdaAlias', { 222 | aliasName: 'OrchestrationLambdaAlias', 223 | version: orchestrationFunction.currentVersion, 224 | provisionedConcurrentExecutions: 1, 225 | }) 226 | return orchestrationFunction; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /lib/constructs/react-app-build.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { Aws, Stack } from "aws-cdk-lib"; 3 | import { IBucket } from "aws-cdk-lib/aws-s3"; 4 | import { 5 | BucketDeployment, 6 | Source as S3Source, 7 | } from "aws-cdk-lib/aws-s3-deployment"; 8 | import { 9 | BuildSpec, 10 | Source, 11 | LinuxBuildImage, 12 | Project, 13 | Artifacts, 14 | } from "aws-cdk-lib/aws-codebuild"; 15 | import { PolicyStatement, Effect, Policy } from "aws-cdk-lib/aws-iam"; 16 | import { Rule } from "aws-cdk-lib/aws-events"; 17 | import { CodeBuildProject } from "aws-cdk-lib/aws-events-targets"; 18 | import { Key } from "aws-cdk-lib/aws-kms"; 19 | 20 | export interface ReactAppProps { 21 | readonly kmsKey: Key; 22 | readonly reactAppBucket: IBucket; 23 | readonly apiUrl: string; 24 | readonly apiKeyParameterName: string; 25 | } 26 | 27 | export class ReactAppBuild extends Construct { 28 | constructor(scope: Construct, id: string, props: ReactAppProps) { 29 | super(scope, id); 30 | 31 | // Define the environment variables to be passed into the React app 32 | const reactEnvironmentVariables: Record = { 33 | VITE_API_URL: props.apiUrl, 34 | VITE_API_KEY: props.apiKeyParameterName, // CodeBuild will inject the value from Parameter Store 35 | }; 36 | 37 | // Deploy the React source code to S3 38 | this.deployReactSourceCode(props.reactAppBucket); 39 | 40 | // Create the shell commands to create the React .env file during the build 41 | const commands = this.createEnvFileCommands(reactEnvironmentVariables); 42 | 43 | // Create a CodeBuild project to build the React application 44 | const codebuildProject = this.createCodeBuildProject( 45 | props.reactAppBucket, 46 | props.kmsKey, 47 | reactEnvironmentVariables, 48 | commands 49 | ); 50 | 51 | // Grant the CodeBuild project permission to access S3 and SSM (for API key) 52 | this.grantCodeBuildAccess(codebuildProject, props); 53 | 54 | // Create an EventBridge rule to trigger CodeBuild on stack deployment 55 | this.createCodeBuildTriggerRule(codebuildProject, props.reactAppBucket); 56 | } 57 | 58 | private deployReactSourceCode(reactAppBucket: IBucket): void { 59 | new BucketDeployment(this, "DeployReactSourceCode", { 60 | destinationBucket: reactAppBucket, 61 | destinationKeyPrefix: "source", 62 | sources: [ 63 | S3Source.asset("./lib/react-app", { 64 | exclude: ["dist", "node_modules", ".env.*"], 65 | }), 66 | ], 67 | }).node.addDependency(reactAppBucket); 68 | } 69 | 70 | private createEnvFileCommands( 71 | environmentVariables: Record 72 | ): string[] { 73 | const commands = []; 74 | for (const [key, value] of Object.entries(environmentVariables)) { 75 | if (key === "VITE_API_KEY") { 76 | commands.push( 77 | `${key}=$(aws ssm get-parameter --name ${value} --query Parameter.Value --output text)` 78 | ); 79 | } 80 | commands.push(`echo "${key}=$${key}" >> .env`); 81 | } 82 | return commands; 83 | } 84 | 85 | private createCodeBuildProject( 86 | reactAppBucket: IBucket, 87 | kmsKey: Key, 88 | environmentVariables: Record, 89 | commands: string[] 90 | ): Project { 91 | const project = new Project(this, "ReactBuildProject", { 92 | projectName: "transcribe-react-build", 93 | encryptionKey: kmsKey, 94 | source: Source.s3({ 95 | bucket: reactAppBucket, 96 | path: "source/", 97 | }), 98 | artifacts: Artifacts.s3({ 99 | bucket: reactAppBucket, 100 | includeBuildId: false, 101 | packageZip: false, 102 | name: "dist", 103 | encryption: true, 104 | }), 105 | buildSpec: BuildSpec.fromObject({ 106 | version: "0.2", 107 | env: { 108 | shell: "bash", 109 | variables: environmentVariables, 110 | }, 111 | phases: { 112 | install: { 113 | "runtime-versions": { 114 | nodejs: 20, 115 | }, 116 | }, 117 | pre_build: { 118 | commands, 119 | }, 120 | build: { 121 | commands: ["npm ci", "npm run build"], 122 | }, 123 | }, 124 | artifacts: { 125 | files: "**/*", 126 | "base-directory": "dist", 127 | }, 128 | }), 129 | environment: { 130 | buildImage: LinuxBuildImage.STANDARD_6_0, 131 | }, 132 | }); 133 | project.node.addDependency(reactAppBucket); 134 | 135 | return project; 136 | } 137 | 138 | private grantCodeBuildAccess( 139 | codebuildProject: Project, 140 | props: ReactAppProps 141 | ): void { 142 | if (codebuildProject.role) { 143 | props.reactAppBucket.grantRead(codebuildProject.role); 144 | codebuildProject.addToRolePolicy( 145 | new PolicyStatement({ 146 | effect: Effect.ALLOW, 147 | actions: ["ssm:GetParameter"], 148 | resources: [ 149 | `arn:aws:ssm:${Aws.REGION}:${Aws.ACCOUNT_ID}:parameter/${props.apiKeyParameterName}`, 150 | ], 151 | }) 152 | ); 153 | } else { 154 | console.error("CodeBuild project role is undefined"); 155 | } 156 | } 157 | 158 | private createCodeBuildTriggerRule( 159 | codebuildProject: Project, 160 | reactAppBucket: IBucket 161 | ): void { 162 | const rule = new Rule(this, "ReactBuildTriggerRule", { 163 | ruleName: "transcribe-react-build-on-stack-create", 164 | eventPattern: { 165 | source: ["aws.cloudformation"], 166 | detailType: ["CloudFormation Stack Status Change"], 167 | detail: { 168 | "stack-id": [Stack.of(this).stackId], 169 | "status-details": { 170 | status: ["CREATE_COMPLETE", "UPDATE_COMPLETE"], 171 | }, 172 | }, 173 | }, 174 | targets: [new CodeBuildProject(codebuildProject)], 175 | description: "Trigger CodeBuild Lambda when React source code is updated", 176 | enabled: true, 177 | }); 178 | rule.node.addDependency(reactAppBucket); 179 | 180 | if (codebuildProject.role) { 181 | const policyStatement = new PolicyStatement({ 182 | effect: Effect.ALLOW, 183 | actions: ["codebuild:StartBuild"], 184 | resources: [codebuildProject.projectArn], 185 | }); 186 | codebuildProject.role.attachInlinePolicy( 187 | new Policy(this, "CodeBuildStartBuildPolicy", { 188 | statements: [policyStatement], 189 | }) 190 | ); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /lib/constructs/react-app-deploy.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { Aws, CfnOutput, Duration, RemovalPolicy } from "aws-cdk-lib"; 3 | import { IBucket } from "aws-cdk-lib/aws-s3"; 4 | import { 5 | CloudFrontWebDistribution, 6 | GeoRestriction, 7 | CfnOriginAccessControl, 8 | CfnDistribution, 9 | } from "aws-cdk-lib/aws-cloudfront"; 10 | import { PolicyStatement, Effect, ServicePrincipal } from "aws-cdk-lib/aws-iam"; 11 | import { CfnWebACL } from "aws-cdk-lib/aws-wafv2"; 12 | import { Key } from "aws-cdk-lib/aws-kms"; 13 | import { NagSuppressions } from "cdk-nag"; 14 | 15 | export interface ReactAppProps { 16 | readonly kmsKey: Key; 17 | readonly reactAppBucket: IBucket; 18 | } 19 | 20 | export class ReactAppDeploy extends Construct { 21 | constructor(scope: Construct, id: string, props: ReactAppProps) { 22 | super(scope, id); 23 | 24 | // Create the CloudFront web application firewall (WAF) 25 | const cloudfrontACL = this.createCloudFrontWaf(); 26 | cloudfrontACL.applyRemovalPolicy(RemovalPolicy.DESTROY); 27 | 28 | // Create the CloudFront distribution 29 | const cloudFrontDistribution = this.createCloudFrontDistribution( 30 | props.reactAppBucket, 31 | cloudfrontACL 32 | ); 33 | 34 | // Add the CloudFront Origin Access Control (OAC) 35 | this.addCloudFrontOriginAccessControl(cloudFrontDistribution); 36 | 37 | // Update the KMS key policy to allow use by CloudFront distribution 38 | this.updateKmsKeyPolicy(props.kmsKey, cloudFrontDistribution); 39 | 40 | // Update the S3 bucket policy to allow access to CloudFront distribution 41 | this.updateS3BucketPolicy(props.reactAppBucket, cloudFrontDistribution); 42 | 43 | // Create the CloudFormation output for the CloudFront URL 44 | this.createCloudFrontDistributionOutput(cloudFrontDistribution); 45 | } 46 | 47 | private createCloudFrontWaf(): CfnWebACL { 48 | return new CfnWebACL(this, "APIAcl", { 49 | defaultAction: { 50 | allow: {}, 51 | }, 52 | scope: "CLOUDFRONT", 53 | visibilityConfig: { 54 | cloudWatchMetricsEnabled: true, 55 | metricName: "MetricForWebACL", 56 | sampledRequestsEnabled: true, 57 | }, 58 | rules: [ 59 | { 60 | name: "CRSRule", 61 | priority: 0, 62 | statement: { 63 | managedRuleGroupStatement: { 64 | name: "AWSManagedRulesCommonRuleSet", 65 | vendorName: "AWS", 66 | }, 67 | }, 68 | visibilityConfig: { 69 | cloudWatchMetricsEnabled: true, 70 | metricName: "MetricForWebACLCDK-CRS", 71 | sampledRequestsEnabled: true, 72 | }, 73 | overrideAction: { 74 | none: {}, 75 | }, 76 | }, 77 | ], 78 | }); 79 | } 80 | 81 | private createCloudFrontDistribution( 82 | reactAppBucket: IBucket, 83 | cloudfrontACL: CfnWebACL 84 | ): CloudFrontWebDistribution { 85 | const distribution = new CloudFrontWebDistribution( 86 | this, 87 | "CloudFrontDistribution", 88 | { 89 | originConfigs: [ 90 | { 91 | s3OriginSource: { 92 | s3BucketSource: reactAppBucket, 93 | originPath: "/dist", 94 | }, 95 | behaviors: [ 96 | { 97 | isDefaultBehavior: true, 98 | minTtl: Duration.seconds(0), 99 | defaultTtl: Duration.seconds(120), 100 | maxTtl: Duration.seconds(300), 101 | forwardedValues: { 102 | queryString: false, 103 | cookies: { 104 | forward: "none", 105 | }, 106 | headers: [ 107 | "Origin", 108 | "Access-Control-Request-Headers", 109 | "Access-Control-Request-Method", 110 | "Cache-Control", 111 | ], 112 | }, 113 | }, 114 | ], 115 | }, 116 | ], 117 | errorConfigurations: [ 118 | { 119 | errorCode: 404, 120 | errorCachingMinTtl: 0, 121 | responseCode: 200, 122 | responsePagePath: "/", 123 | }, 124 | ], 125 | loggingConfig: { 126 | bucket: reactAppBucket, 127 | prefix: "access-logs/", 128 | }, 129 | webACLId: cloudfrontACL.attrArn, 130 | geoRestriction: GeoRestriction.allowlist("US", "CA"), 131 | } 132 | ); 133 | 134 | NagSuppressions.addResourceSuppressions(distribution, [ 135 | { 136 | id: "AwsSolutions-CFR4", 137 | reason: "SSL version not enforced since this is a demo application", 138 | }, 139 | ]); 140 | 141 | return distribution; 142 | } 143 | 144 | private addCloudFrontOriginAccessControl( 145 | cloudFrontDistribution: CloudFrontWebDistribution 146 | ): void { 147 | const oac = new CfnOriginAccessControl(this, "OriginAccessControl", { 148 | originAccessControlConfig: { 149 | name: "transcribe-oac", 150 | originAccessControlOriginType: "s3", 151 | signingBehavior: "always", 152 | signingProtocol: "sigv4", 153 | }, 154 | }); 155 | const cfnDistribution = cloudFrontDistribution.node 156 | .defaultChild as CfnDistribution; 157 | cfnDistribution.addPropertyOverride( 158 | "DistributionConfig.Origins.0.OriginAccessControlId", 159 | oac.getAtt("Id") 160 | ); 161 | } 162 | 163 | private updateKmsKeyPolicy( 164 | kmsKey: Key, 165 | cloudFrontDistribution: CloudFrontWebDistribution 166 | ): void { 167 | kmsKey.addToResourcePolicy( 168 | new PolicyStatement({ 169 | effect: Effect.ALLOW, 170 | principals: [new ServicePrincipal("cloudfront.amazonaws.com")], 171 | actions: ["kms:Decrypt", "kms:Encrypt", "kms:GenerateDataKey*"], 172 | resources: ["*"], 173 | conditions: { 174 | StringEquals: { 175 | "aws:SourceArn": `arn:aws:cloudfront::${Aws.ACCOUNT_ID}:distribution/${cloudFrontDistribution.distributionId}`, 176 | }, 177 | }, 178 | }) 179 | ); 180 | } 181 | 182 | private updateS3BucketPolicy( 183 | reactAppBucket: IBucket, 184 | cloudFrontDistribution: CloudFrontWebDistribution 185 | ): void { 186 | const bucketPolicy = new PolicyStatement({ 187 | effect: Effect.ALLOW, 188 | principals: [new ServicePrincipal("cloudfront.amazonaws.com")], 189 | actions: ["s3:GetObject"], 190 | resources: [`${reactAppBucket.bucketArn}/*`], 191 | conditions: { 192 | StringEquals: { 193 | "AWS:SourceArn": `arn:aws:cloudfront::${Aws.ACCOUNT_ID}:distribution/${cloudFrontDistribution.distributionId}`, 194 | }, 195 | }, 196 | }); 197 | reactAppBucket.addToResourcePolicy(bucketPolicy); 198 | } 199 | 200 | private createCloudFrontDistributionOutput( 201 | cloudFrontDistribution: CloudFrontWebDistribution 202 | ): void { 203 | new CfnOutput(this, "CloudFrontDistributionUrl", { 204 | key: "ReactAppUrl", 205 | value: `https://${cloudFrontDistribution.distributionDomainName}`, 206 | description: "The CloudFront URL for the React App", 207 | }); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/constructs/s3.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { Aws, CfnOutput, RemovalPolicy } from "aws-cdk-lib"; 3 | import { 4 | Bucket, 5 | BucketEncryption, 6 | BlockPublicAccess, 7 | ObjectOwnership, 8 | BucketProps, 9 | } from "aws-cdk-lib/aws-s3"; 10 | import { PolicyStatement, Effect, AnyPrincipal } from "aws-cdk-lib/aws-iam"; 11 | import { NagSuppressions } from "cdk-nag"; 12 | 13 | export class S3Buckets extends Construct { 14 | public readonly documentBucket: Bucket; 15 | public readonly reactAppBucket: Bucket; 16 | 17 | constructor(scope: Construct, id: string) { 18 | super(scope, id); 19 | 20 | // Create a bucket for PDF document output 21 | this.documentBucket = this.createS3Bucket( 22 | "DocumentBucket", 23 | `transcribe-documents-${Aws.ACCOUNT_ID}`, 24 | { 25 | objectOwnership: undefined, 26 | } 27 | ); 28 | 29 | // Enforce HTTPS on the document bucket 30 | this.addDocumentBucketPolicy(this.documentBucket); 31 | 32 | // Create a bucket for the React user interface 33 | this.reactAppBucket = this.createS3Bucket( 34 | "ReactAppBucket", 35 | `transcribe-react-app-${Aws.ACCOUNT_ID}`, 36 | { 37 | objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED, 38 | } 39 | ); 40 | 41 | // Create CloudFormation outputs for the bucket names 42 | this.createOutputs(); 43 | } 44 | 45 | private createS3Bucket( 46 | id: string, 47 | bucketName: string, 48 | overrides: Partial 49 | ): Bucket { 50 | const bucket = new Bucket(this, id, { 51 | bucketName, 52 | encryption: BucketEncryption.S3_MANAGED, 53 | enforceSSL: true, 54 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 55 | removalPolicy: RemovalPolicy.DESTROY, 56 | autoDeleteObjects: true, 57 | ...overrides, 58 | }); 59 | 60 | NagSuppressions.addResourceSuppressions(bucket, [ 61 | { 62 | id: "AwsSolutions-S1", 63 | reason: "Server access logs not required for demo application", 64 | }, 65 | ]); 66 | 67 | return bucket; 68 | } 69 | 70 | private addDocumentBucketPolicy(bucket: Bucket): void { 71 | const policy = new PolicyStatement({ 72 | effect: Effect.DENY, 73 | actions: ["s3:*"], 74 | resources: [bucket.bucketArn, `${bucket.bucketArn}/*`], 75 | conditions: { 76 | Bool: { "aws:SecureTransport": "false" }, 77 | }, 78 | principals: [new AnyPrincipal()], 79 | }); 80 | bucket.addToResourcePolicy(policy); 81 | } 82 | 83 | private createOutputs(): void { 84 | new CfnOutput(this, "DocumentBucketName", { 85 | key: "DocumentsS3Bucket", 86 | value: this.documentBucket.bucketName, 87 | description: "The name of the documents S3 bucket", 88 | }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/lambda-functions/get_credentials/lambda_handler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import boto3 4 | 5 | sts_client = boto3.client("sts") 6 | 7 | 8 | def lambda_handler(_event, _context): 9 | role_arn = os.environ["ROLE_ARN"] 10 | 11 | try: 12 | # Assume the role 13 | response = sts_client.assume_role( 14 | RoleArn=role_arn, RoleSessionName="TranscribeSession" 15 | ) 16 | 17 | # Extract the temporary credentials 18 | credentials = response["Credentials"] 19 | credentials["Expiration"] = credentials["Expiration"].isoformat() 20 | 21 | # Return the temporary credentials 22 | return { 23 | "statusCode": 200, 24 | "body": json.dumps( 25 | { 26 | "AccessKeyId": credentials["AccessKeyId"], 27 | "SecretAccessKey": credentials["SecretAccessKey"], 28 | "SessionToken": credentials["SessionToken"], 29 | "Expiration": credentials["Expiration"], 30 | "Region": os.environ["AWS_REGION"], 31 | } 32 | ), 33 | "headers": { 34 | "Access-Control-Allow-Origin": "*", 35 | "Access-Control-Allow-Headers": "Content-Type", 36 | "Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE", 37 | }, 38 | } 39 | except Exception as e: 40 | return { 41 | "statusCode": 500, 42 | "body": json.dumps( 43 | { 44 | "message": "Error assuming role", 45 | "error": str(e), 46 | } 47 | ), 48 | "headers": { 49 | "Access-Control-Allow-Origin": "*", 50 | "Access-Control-Allow-Headers": "Content-Type", 51 | "Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE", 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/Dockerfile: -------------------------------------------------------------------------------- 1 | #checkov:skip=CKV_DOCKER_2:Using AWS Lambda container image 2 | #checkov:skip=CKV_DOCKER_3:Base image from AWS already uses limited user 3 | FROM public.ecr.aws/lambda/python:3.12@sha256:cc5291811aa5dc859d25f8e12075f8c51d351edb5e00774e171b3e474bc455ad 4 | RUN dnf install -y pango-1.48.10 && dnf clean all 5 | COPY . ${LAMBDA_TASK_ROOT} 6 | RUN pip install -r requirements.txt --no-cache-dir 7 | CMD ["summarize_generate.lambda_handler"] -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/README.md: -------------------------------------------------------------------------------- 1 | # Orchestration Lambda 2 | 3 | ## Introduction 4 | 5 | The primary objective of this AWS Lambda is to generate summary of multiple relevant answers received for a given question. This AWS Lambda uses Large Language Models from Amazon Bedrock service to generate the summary. 6 | 7 | ## Component Details 8 | 9 | #### Prerequisites 10 | 11 | - [Python 3.12](https://www.python.org/downloads/release/python-3120/) or later 12 | - [AWS Lambda Powertools 2.35.1](https://docs.powertools.aws.dev/lambda/python/2.35.1/) 13 | 14 | #### Technology stack 15 | 16 | - [AWS Lambda](https://aws.amazon.com/lambda/) 17 | - [Amazon Bedrock](https://aws.amazon.com/bedrock/) 18 | - [Amazon S3](https://aws.amazon.com/s3/) 19 | 20 | #### Package Details 21 | 22 | | Files | Description | 23 | | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | 24 | | [connections.py](connections.py) | Python file with `Connections` class for establishing connections with external dependencies of the lambda | 25 | | [exceptions.py](exceptions.py) | Python file containing custom exception classes `CodeError` and `ConnectionError` | 26 | | [document_generator.py](document_generator.py) | Python file containing the helper functions that convert HTML to PDF files | 27 | | [generate.py](generate.py) | Python file containing the helper functions that generate PDF files and upload them to S3 bucket | 28 | | [prompt_templates.py](prompt_templates.py) | Python variables with input Prompts for the LLM to operate | 29 | | [summarization.py](summarization.py) | Python utility class for performing answer summary using Amazon Bedrock service | 30 | | [summarize_generate.py](summarize_generate.py) | Python file containing `lambda_handler` | 31 | | [utils.py](utils.py) | Python utility file containing reusable methods | 32 | | [requirements.txt](requirements.txt) | A text file containing all dependencies for this lambda | 33 | 34 | #### Input 35 | 36 | ```json 37 | { 38 | "documentName": "test-live-knowledge-capture", 39 | "questionText": "What is Amazon SageMaker?", 40 | "documentText": "**Amazon SageMaker** is a fully managed machine learning service that enables developers and data scientists to quickly build, train, and deploy machine learning models at scale.", 41 | "audioFiles": ["base64string...", "..."] 42 | } 43 | ``` 44 | 45 | | Field | Description | Data Type | 46 | | -------------- | -------------------------------------------------------------- | --------- | 47 | | `documentName` | User input document name | String | 48 | | `questionText` | User's question to be answered | String | 49 | | `documentText` | The answer to users question capture by Amazon Transcribe Live | String | 50 | | `audioFiles ` | The recorded audio clips as base64 encoded strings | String[] | 51 | 52 | #### Output 53 | 54 | ```json 55 | { 56 | "statusCode": int, 57 | "pdfFileS3Uri": str, 58 | "audioS3Uris": str[], 59 | "documentName": str, 60 | "serviceName": "genai-knowledge-capture-transcribe-live" 61 | } 62 | ``` 63 | 64 | | Field | Description | Data Type | 65 | | -------------- | ---------------------------------------------------------------------------------------------------------------------- | --------- | 66 | | `statusCode` | A HTTP status code that denotes the output status of validation. A `200` value means validation completed successfully | Number | 67 | | `pdfFileS3Uri` | S3 uri of the generated PDF file | String | 68 | | `audioS3Uris` | S3 uris of the saved audio files | String[] | 69 | | `documentName` | User input document name | String | 70 | | `serviceName` | The name of the AWS Lambda as configured through AWS Powertools across log statements | String | 71 | 72 | #### Environmental Variables 73 | 74 | | Field | Description | Data Type | 75 | | ------------------------------ | --------------------------------------------------------------- | --------- | 76 | | `POWERTOOLS_LOG_LEVEL` | Sets how verbose Logger should be (INFO, by default) | String | 77 | | `DATA_SOURCE_BUCKET_NAME` | S3 bucket where the final PDF file is stored | String | 78 | | `POWERTOOLS_SERVICE_NAME` | Sets service key that will be present across all log statements | String | 79 | | `POWERTOOLS_METRICS_NAMESPACE` | Sets namespace key that will be present across metrics log | String | 80 | | `AWS_REGION` | AWS Region where the solution is deployed | String | 81 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/connections.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | from aws_lambda_powertools import Logger, Tracer, Metrics 4 | from langchain_aws import ChatBedrock 5 | from botocore.client import Config 6 | 7 | tracer = Tracer() 8 | logger = Logger(log_uncaught_exceptions=True, serialize_stacktrace=True) 9 | metrics = Metrics() 10 | 11 | MODEL_ID_MAPPING = { 12 | "Titan": "amazon.titan-tg1-large", 13 | "Claude2": "anthropic.claude-v2", 14 | "ClaudeInstant": "anthropic.claude-instant-v1", 15 | "Claude3Sonnet": "anthropic.claude-3-sonnet-20240229-v1:0", 16 | "Claude3Haiku": "anthropic.claude-3-haiku-20240307-v1:0", 17 | } 18 | 19 | 20 | class Connections: 21 | """ 22 | A class to maintain connections to external dependencies 23 | 24 | Attributes 25 | ---------- 26 | region_name : str 27 | The AWS Region name where the AWS Lambda function is running. 28 | Depends on the environmental variable 'AWS_REGION' 29 | s3_bucket_name : str 30 | Name of the S3 bucket to use for storing the generated documents. 31 | service_name: str 32 | Name of the service assigned and configured through AWS Powertools for 33 | logging. Depends on the environmental variable 'POWERTOOLS_SERVICE_NAME' 34 | region_name : str 35 | The AWS Region name where the AWS Lambda function is running. 36 | Depends on the environmental variable 'AWS_REGION' 37 | s3_client : boto3.client 38 | Boto3 client to interact with AWS S3 bucket 39 | bedrock_runtime_client : boto3.client 40 | Boto3 client to interact with AWS Bedrock Runtime 41 | 42 | Methods 43 | ------- 44 | get_bedrock_llm(max_tokens=256, model_id="ClaudeX") 45 | """ 46 | 47 | namespace = os.environ["POWERTOOLS_METRICS_NAMESPACE"] 48 | service_name = os.environ["POWERTOOLS_SERVICE_NAME"] 49 | region_name = os.environ["AWS_REGION"] 50 | s3_bucket_name = os.environ["S3_BUCKET_NAME"] 51 | 52 | s3_client = boto3.client("s3", region_name=region_name) 53 | 54 | config = Config(read_timeout=1000) 55 | bedrock_runtime_client = boto3.client( 56 | "bedrock-runtime", region_name=region_name, config=config 57 | ) 58 | 59 | @staticmethod 60 | def get_bedrock_llm(max_tokens=256, model_name="Claude3Sonnet"): 61 | """ 62 | Create and return the bedrock instance with the llm model to use. 63 | 64 | Args: None. 65 | 66 | Returns: 67 | Bedrock instance with the llm model to use. 68 | """ 69 | model_kwargs_mapping = { 70 | "Titan": { 71 | "maxTokenCount": max_tokens, 72 | "temperature": 0, 73 | "topP": 1, 74 | }, 75 | "Claude2": { 76 | "max_tokens": max_tokens, 77 | "temperature": 0, 78 | "top_p": 1, 79 | "top_k": 50, 80 | "stop_sequences": ["\n\nHuman"], 81 | }, 82 | "ClaudeInstant": { 83 | "max_tokens": max_tokens, 84 | "temperature": 0, 85 | "top_p": 1, 86 | "top_k": 50, 87 | "stop_sequences": ["\n\nHuman"], 88 | }, 89 | "Claude3Sonnet": { 90 | "max_tokens": max_tokens, 91 | "temperature": 0, 92 | "top_p": 1, 93 | "top_k": 50, 94 | "stop_sequences": ["\n\nHuman"], 95 | }, 96 | "Claude3Haiku": { 97 | "max_tokens": max_tokens, 98 | "temperature": 0, 99 | "top_p": 1, 100 | "top_k": 50, 101 | "stop_sequences": ["\n\nHuman"], 102 | }, 103 | } 104 | 105 | return ChatBedrock( 106 | client=Connections.bedrock_runtime_client, 107 | model_id=MODEL_ID_MAPPING[model_name], 108 | model_kwargs=model_kwargs_mapping[model_name], 109 | ) 110 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/document_generator.py: -------------------------------------------------------------------------------- 1 | from weasyprint import HTML 2 | from exceptions import CodeError 3 | from dominate.util import raw 4 | from dominate.tags import html, head, style, body, h1, h2, u 5 | from connections import logger 6 | import markdown 7 | import os 8 | from time import localtime, strftime 9 | 10 | # Stylesheet to use for rendering the final PDF document 11 | STYLE_CSS = """ 12 | h1 {{ 13 | position: center; 14 | }} 15 | header {{ 16 | position: running(header); 17 | }} 18 | @page {{ 19 | size: Letter portrait; 20 | margin-top: 3cm; 21 | 22 | @top-right {{ 23 | content: '{0}'; 24 | font-size: 10px; 25 | }} 26 | @bottom-center {{ 27 | content: counter(page); 28 | }} 29 | @bottom-right {{ 30 | content: 'Document generated using AWS Bedrock Service'; 31 | font-size: 10px; 32 | }} 33 | }} 34 | """ 35 | 36 | 37 | def markdown_to_html(markdown_text: str) -> str: 38 | """ 39 | Converts Markdown text to HTML 40 | 41 | Arguments: 42 | ---------- 43 | markdown_text: str 44 | Markdown text to be converted to HTML 45 | 46 | Returns: 47 | -------- 48 | html: str 49 | HTML document generated from the Markdown text 50 | """ 51 | logger.debug(f"Markdown document generated for body {markdown_text}") 52 | html = markdown.markdown(markdown_text) 53 | logger.debug(f"HTML document generated for body {html}") 54 | return html 55 | 56 | 57 | def generate_html(html_body: str) -> str: 58 | """ 59 | Generates a new HTML document based in the HTML Body data passed. 60 | This function is used to generate the HTML document for the PDF generation. 61 | 62 | Arguments: 63 | ----------- 64 | html_body: str 65 | HTML body data to be used to generate the HTML document 66 | 67 | Returns: 68 | -------- 69 | str: 70 | HTML document generated from the HTML body data passed. 71 | This document will be used to generate the PDF file. The 72 | document is rendered in string encoding 73 | """ 74 | logger.debug(f"HTML document generated for body {html_body}") 75 | 76 | current_time = strftime("%m-%d-%Y %H:%M:%S", localtime()) 77 | html_doc = html() 78 | html_doc.add(head(style(STYLE_CSS.format(current_time)))) 79 | html_doc.add(body(raw(html_body))) 80 | 81 | return html_doc.render() 82 | 83 | 84 | def html_to_pdf(html_document: str, pdf_path: str) -> None: 85 | """ 86 | Converts given HTML document to PDF file. 87 | 88 | Arguments: 89 | ----------- 90 | html_document: str 91 | HTML document, in string encoded format, to be converted to PDF file 92 | pdf_path: str 93 | Path where the PDF file will be generated. 94 | """ 95 | try: 96 | HTML(string=html_document, base_url=os.getcwd()).write_pdf(pdf_path) 97 | logger.debug(f"PDF file generated at: {pdf_path}") 98 | except Exception as exception: 99 | raise CodeError(f"Error while generating PDF file: {exception}") 100 | return None 101 | 102 | 103 | def add_header(header_name: str) -> str: 104 | """ 105 | Adds a header to the HTML document. 106 | 107 | Arguments: 108 | ----------- 109 | header_name: str 110 | Name of the header to be added to the HTML document. 111 | 112 | Returns: 113 | -------- 114 | str: 115 | HTML document encoded as string with the header added as H2 tag. 116 | """ 117 | return h2(header_name).render() 118 | 119 | 120 | def add_document_title(title_text: str) -> str: 121 | """ 122 | Adds document title to the HTML document. 123 | 124 | Arguments: 125 | ---------- 126 | title_text: str 127 | Name of the title to be added to the HTML document. 128 | 129 | Returns: 130 | -------- 131 | str: 132 | HTML document encoded as string with the title text added as H1 tag. 133 | """ 134 | return h1(u(title_text), style="text-align: center;").render() 135 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/exceptions.py: -------------------------------------------------------------------------------- 1 | class ConnectionError(Exception): 2 | """An exception class for connection related errors""" 3 | 4 | def __init__(self, message): 5 | self.message = message 6 | 7 | def __str__(self): 8 | return str(self.message) 9 | 10 | 11 | class CodeError(Exception): 12 | """An exception class for code/logic related errors""" 13 | 14 | def __init__(self, message): 15 | self.message = message 16 | 17 | def __str__(self): 18 | return str(self.message) 19 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/generate.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import datetime 3 | from aws_lambda_powertools.utilities.parser import BaseModel 4 | from document_generator import ( 5 | markdown_to_html, 6 | generate_html, 7 | html_to_pdf, 8 | add_document_title, 9 | ) 10 | from utils import generate_presigned_url 11 | from connections import Connections, tracer, logger 12 | from dataclasses import dataclass 13 | from exceptions import CodeError 14 | import tempfile 15 | 16 | s3_client = Connections.s3_client 17 | 18 | 19 | @dataclass 20 | class Response: 21 | """A class for representing the Output format.""" 22 | 23 | statusCode: int 24 | pdfFileS3Uri: str 25 | audioS3Uris: list[str] 26 | documentName: str 27 | serviceName: str = Connections.service_name 28 | 29 | 30 | class Request(BaseModel): 31 | """A class for representing the Input format.""" 32 | 33 | documentName: str 34 | documentText: str 35 | audioFiles: int 36 | 37 | 38 | def generate_document( 39 | document_name: str, document_title: str, document_text: str, audio_files: list[str] 40 | ) -> Response: 41 | """Generate a document and upload it to S3.""" 42 | try: 43 | html_body = render_html_body(document_title, document_text) 44 | html_content = generate_html(html_body) 45 | pdf_file_path = generate_pdf(html_content) 46 | logger.debug(f"Generated PDF: {pdf_file_path}") 47 | 48 | session_id = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d_%H-%M-%S_%f") 49 | pdf_s3_url = upload_pdf_to_s3(session_id, pdf_file_path, document_name) 50 | audio_s3_urls = upload_audio_to_s3(session_id, document_name, audio_files) 51 | 52 | return Response( 53 | statusCode=200, 54 | pdfFileS3Uri=pdf_s3_url, 55 | audioS3Uris=audio_s3_urls, 56 | documentName=document_name, 57 | serviceName=Connections.service_name, 58 | ) 59 | except Exception as e: 60 | logger.warning(f"Error generating document: {e}") 61 | raise CodeError(f'Error generating document: {e}') from e 62 | 63 | 64 | def generate_pdf(html_content: str) -> str: 65 | """Generate PDF from HTML content.""" 66 | logger.info(f"Generating PDF for html content {html_content}") 67 | with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_pdf: 68 | temp_pdf_name = temp_pdf.name 69 | temp_pdf.write(html_content.encode('utf-8')) 70 | 71 | html_to_pdf(html_content, temp_pdf_name) 72 | 73 | return temp_pdf_name 74 | 75 | 76 | def upload_pdf_to_s3(session_id: str, file_path: str, document_name: str) -> str: 77 | """Upload a file to S3 and return its S3 URL.""" 78 | logger.info(f"Uploading PDF to S3: {file_path}") 79 | bucket = Connections.s3_bucket_name 80 | key = f"{session_id}/{document_name}.pdf" 81 | 82 | s3_client.upload_file( 83 | file_path, bucket, key, ExtraArgs={"ContentType": "application/pdf"} 84 | ) 85 | s3_url = f"s3://{bucket}/{key}" 86 | presigned_url = generate_presigned_url(s3_client, bucket, key) 87 | 88 | logger.info(f"Uploaded PDF to S3: {s3_url}") 89 | 90 | return presigned_url 91 | 92 | 93 | def upload_audio_to_s3(session_id: str, document_name: str, audio_files: list[str]) -> list[str]: 94 | """Upload a file to S3 and return its S3 URL.""" 95 | logger.info(f"Uploading audio to S3: {audio_files}") 96 | bucket = Connections.s3_bucket_name 97 | presigned_urls = [] 98 | for index, audio_file in enumerate(audio_files): 99 | key = f"{session_id}/{document_name}_audio_{index}.webm" 100 | 101 | # Decode the base64-encoded audio data 102 | audio_data = base64.b64decode(audio_file) 103 | 104 | # Upload the audio data to S3 105 | s3_client.put_object( 106 | Bucket=bucket, 107 | Key=key, 108 | Body=audio_data, 109 | ContentType='audio/webm' 110 | ) 111 | s3_url = f"s3://{bucket}/{key}" 112 | presigned_url = generate_presigned_url(s3_client, bucket, key) 113 | presigned_urls.append(presigned_url) 114 | 115 | logger.info(f"Uploaded audio file to S3: {s3_url}") 116 | 117 | return presigned_urls 118 | 119 | 120 | @tracer.capture_method 121 | def render_html_body(document_title: str, document_text: str) -> str: 122 | """Generate HTML body from the document text.""" 123 | document_body = "" + add_document_title(document_title) 124 | try: 125 | logger.info("Building document section with text summary") 126 | text = bytes(document_text, "utf-8").decode("unicode_escape") 127 | if text.startswith('"') and text.endswith('"'): 128 | text = text[1:-1] 129 | document_body += markdown_to_html(text.strip()) 130 | except Exception as exception: 131 | logger.warning(f"Error while generating HTML body: {exception}") 132 | raise CodeError(f'Error while generating HTML body: {exception}') from exception 133 | 134 | return document_body 135 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/prompt_templates.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = """ 2 | You are an AI language model assistant specialized in summarizing a list of input texts into a coherent version. 3 | I'm going to give you a list of input texts. Your task is to merge the input texts together and do a coherent summarization relating to the input question. 4 | Your output answer should be as detailed as possible. 5 | """ 6 | 7 | 8 | SUMMARIZATION_TEMPLATE_PARAGRAPH = """ 9 | Here is the list of input texts: 10 | 11 | 12 | {input_texts} 13 | 14 | 15 | First, review the given list of input text as a whole. 16 | Then, summarize all of the input texts into one or multiple paragraphs based on its logic. 17 | Last, double check if there are any key information missed before outputting the final answer. 18 | 19 | Here is the input question: 20 | 21 | 22 | {input_question} 23 | 24 | 25 | Output guidance: 26 | - Never set up any preambles. 27 | - The final answer should be in the style of professional technical report. 28 | - The final answer should be in Markdown format, and emphasize the key phrases or identities using bold font. 29 | - Please conclude your response by succinctly recapping the main points at the beginning of your final output, without using the phrase 'In summary'. 30 | - Start your response with an overview paragraph highlighting the key points, avoiding the phrase 'In summary'. Follow this with detailed explanations, ensuring the final summary is at the beginning and conclusions are woven into the narrative without using bullet points to start. 31 | - Please enclose the final answer in XML tags, with root tag as . Use to indicate the final answer. 32 | 33 | REMEMBER: Never use phrases like 'input texts' or 'To answer the question' or 'in summary' in the final answer! 34 | """ 35 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-lambda-powertools[tracer,parser]==3.2.0 2 | langchain==0.3.7 3 | langchain-aws==0.2.6 4 | langchain-community==0.3.5 5 | defusedxml==0.7.1 6 | weasyprint==62.3 7 | markdown==3.6 8 | boto3>=1.34.69 9 | dominate==2.9.1 10 | pyarrow 11 | s3fs -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/summarization.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from langchain.output_parsers import XMLOutputParser 3 | from langchain_core.prompts import ( 4 | ChatPromptTemplate, 5 | HumanMessagePromptTemplate, 6 | SystemMessagePromptTemplate, 7 | ) 8 | from prompt_templates import SYSTEM_PROMPT, SUMMARIZATION_TEMPLATE_PARAGRAPH 9 | from connections import Connections 10 | from utils import parse_summary 11 | 12 | 13 | def summarization( 14 | question: str, list_of_answers: List[str], model_name: str = "Claude3" 15 | ) -> str: 16 | """ 17 | Summarizes a list of answers for a given question using a specified LLM from Bedrock. 18 | 19 | This function creates a chain of operations including prompt templating, invoking a LLM , and parsing the output, 20 | to generate a summarized response based on the provided answers. 21 | 22 | Inputs: 23 | - question (str): The question for which the answers need to be summarized. 24 | - list_of_answers (List[str]): A list of answers provided for the question. 25 | - model_name (str, optional): The name of the language model to be used for summarization. Defaults to "Claude3" 26 | Returns: 27 | - ans (str): The summarized answer as returned by the language model's output stroutputparser. 28 | 29 | """ 30 | 31 | # Initialize output parser object, specifying the tags (to be consistent with the prompt) 32 | parser = XMLOutputParser(tags=["Output", "Summary"]) 33 | # Prompt from a template & parser 34 | system_message_template = SystemMessagePromptTemplate.from_template(SYSTEM_PROMPT) 35 | human_message_template = HumanMessagePromptTemplate.from_template( 36 | SUMMARIZATION_TEMPLATE_PARAGRAPH 37 | ) 38 | prompt = ChatPromptTemplate.from_messages( 39 | [system_message_template, human_message_template] 40 | ) 41 | # LLM object 42 | llm = Connections.get_bedrock_llm(max_tokens=2048, model_name=model_name) 43 | 44 | # Chain the elements together 45 | chain = prompt | llm | parser 46 | 47 | # Define input dict 48 | input_dict = { 49 | "input_texts": list_of_answers, 50 | "format_instructions": parser.get_format_instructions(), 51 | "input_question": question, 52 | } 53 | 54 | # Invoke the chain 55 | ans = chain.invoke(input_dict) 56 | 57 | ans = parse_summary(ans) 58 | return ans 59 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/summarize_generate.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from typing import Literal 4 | from dataclasses import dataclass, field 5 | from summarization import summarization 6 | from generate import generate_document 7 | from connections import Connections, tracer, logger, metrics 8 | from aws_lambda_powertools.metrics import MetricUnit 9 | from aws_lambda_powertools.utilities.typing import LambdaContext 10 | from aws_lambda_powertools.utilities.parser import BaseModel 11 | 12 | 13 | @dataclass 14 | class Response: 15 | statusCode: int 16 | body: str 17 | headers: dict = field( 18 | default_factory=lambda: { 19 | "Access-Control-Allow-Origin": "*", 20 | "Access-Control-Allow-Credentials": "true", 21 | "Content-Type": "application/json", 22 | } 23 | ) 24 | 25 | 26 | class Request(BaseModel): 27 | documentName: str 28 | questionText: str 29 | documentText: str 30 | audioFiles: list[str] 31 | serviceName: str = Connections.service_name 32 | 33 | 34 | @logger.inject_lambda_context(log_event=True, clear_state=True) 35 | @tracer.capture_lambda_handler 36 | @metrics.log_metrics(capture_cold_start_metric=True) 37 | def lambda_handler(event: Request, _context: LambdaContext) -> str: 38 | metrics.add_metric( 39 | name="TotalSummarizationInvocation", unit=MetricUnit.Count, value=1 40 | ) 41 | logger.info(f"Summarization event: {event}") 42 | 43 | if "body" in event: 44 | payload = json.loads(event["body"]) 45 | else: 46 | payload = event 47 | request = Request(**payload) 48 | document_name = request.documentName 49 | document_text = request.documentText 50 | question = request.questionText 51 | audio_files = request.audioFiles 52 | 53 | # Summarizing answers 54 | if document_text: 55 | # Start timer 56 | start_time = time.time() 57 | 58 | # Calling LLM to summarize the answers for the given question 59 | summary_text = summarization( 60 | question, [document_text], model_name="Claude3Haiku" 61 | ) 62 | logger.debug(f"Summarized answer: \n {summary_text}") 63 | 64 | # End timer 65 | end_time = time.time() 66 | 67 | # Calculate response time in seconds 68 | response_time = end_time - start_time 69 | 70 | # Add metrics 71 | metrics.add_metric( 72 | name="SummarizationLLMResponseTime", 73 | unit=MetricUnit.Seconds, 74 | value=response_time, 75 | ) 76 | 77 | # Generate Document 78 | doc_response = generate_document( 79 | document_name, question, summary_text, audio_files) 80 | 81 | statusCode: Literal[200] | Literal[400] = ( 82 | 200 if doc_response.statusCode == 200 else 400 83 | ) 84 | pdfFileS3Uri: str = doc_response.pdfFileS3Uri 85 | audioS3Uris: list[str] = doc_response.audioS3Uris 86 | 87 | else: 88 | logger.info("No valid answers retrieved for question") 89 | statusCode = 400 90 | pdfFileS3Uri = None 91 | audioS3Uris = None 92 | 93 | response_body = { 94 | "pdfFileS3Uri": pdfFileS3Uri, 95 | "audioS3Uris": audioS3Uris, 96 | "documentName": document_name 97 | } 98 | 99 | response = Response( 100 | statusCode=statusCode, 101 | body=json.dumps(response_body), 102 | ).__dict__ 103 | 104 | logger.info(f"Lambda Output: {response}") 105 | 106 | return response 107 | -------------------------------------------------------------------------------- /lib/lambda-functions/orchestration/utils.py: -------------------------------------------------------------------------------- 1 | from connections import logger 2 | 3 | 4 | def format_inputs(input_texts): 5 | """ 6 | To format a list of input texts into the format that fit into the prompt for Claude models 7 | 8 | Args: 9 | input_texts: a list of input_texts 10 | 11 | Returns: 12 | a str 13 | 14 | """ 15 | 16 | input_text_list = [] 17 | for ( 18 | i, 19 | text, 20 | ) in enumerate(input_texts): 21 | prefix = f"" 22 | suffix = f"" 23 | input_text = prefix + text + suffix + "\n\n" 24 | input_text_list.append(input_text) 25 | 26 | return " ".join(input_text_list) 27 | 28 | 29 | def parse_summary(summary): 30 | """ 31 | Parse the output summary from XMLParser 32 | 33 | Args: 34 | summary: a JSON file from XMLParser output 35 | 36 | Returns: 37 | summary_text (str) 38 | """ 39 | try: 40 | summary_text = summary["Output"][0]["Summary"] 41 | except Exception as e: 42 | logger.debug("An error occurred when parse summary:", e) 43 | return summary_text 44 | 45 | 46 | def generate_presigned_url(s3_client, bucket, key): 47 | """ 48 | Generate a pre-signed URL for an S3 object. 49 | 50 | Args: 51 | s3_client (boto3.client): An S3 client instance. 52 | bucket (str): The name of the S3 bucket. 53 | key (str): The key (filename) of the S3 object. 54 | 55 | Returns: 56 | str: The pre-signed URL for the S3 object. 57 | """ 58 | url = s3_client.generate_presigned_url( 59 | ClientMethod="get_object", 60 | Params={"Bucket": bucket, "Key": key}, 61 | ExpiresIn=86400, 62 | ) 63 | return url 64 | -------------------------------------------------------------------------------- /lib/react-app/.env.template: -------------------------------------------------------------------------------- 1 | # Duplicate this file to ".env.local" for local development and 2 | # set the value(s) below to match the CDK/CloudFormation outputs. 3 | # In the deployed application, the values will be automatically set. 4 | # 5 | # VITE_API_URL - The URL of the API Gateway endpoint. Copy it from 6 | # the CloudFormation outputs. 7 | # VITE_API_KEY - The API key to use for authenticating API requests. 8 | # Copy it from SSM Parameter Store. 9 | # 10 | VITE_API_URL="" 11 | VITE_API_KEY="" 12 | -------------------------------------------------------------------------------- /lib/react-app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /lib/react-app/.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 | -------------------------------------------------------------------------------- /lib/react-app/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /lib/react-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Knowledge Capture Demo with GenAI and Live Transcribe 7 | 8 | 9 | 10 |
11 | 12 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app", 3 | "private": true, 4 | "version": "0.0.0", 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-sdk/client-transcribe-streaming": "^3.529.0", 14 | "@cloudscape-design/components": "^3.0.662", 15 | "@cloudscape-design/global-styles": "^1.0.27", 16 | "fast-xml-parser": "^4.4.1", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.2.66", 22 | "@types/react-dom": "^18.2.22", 23 | "@typescript-eslint/eslint-plugin": "^7.2.0", 24 | "@typescript-eslint/parser": "^7.2.0", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "eslint": "^8.57.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.4.6", 29 | "typescript": "^5.2.2", 30 | "vite": "^6.3.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/react-app/public/worklets/audio-processor.js: -------------------------------------------------------------------------------- 1 | // Based on sample from 2 | // https://github.com/GoogleChromeLabs/web-audio-samples/blob/main/src/audio-worklet/migration/worklet-recorder/audio-processor.js 3 | 4 | class AudioProcessor extends AudioWorkletProcessor { 5 | constructor(options) { 6 | super(); 7 | this.sampleRate = 0; 8 | this.maxRecordingFrames = 0; 9 | this.numberOfChannels = 0; 10 | 11 | if (options && options.processorOptions) { 12 | const { 13 | numberOfChannels, 14 | sampleRate, 15 | maxFrameCount, 16 | } = options.processorOptions; 17 | 18 | this.sampleRate = sampleRate; 19 | this.maxRecordingFrames = maxFrameCount; 20 | this.numberOfChannels = numberOfChannels; 21 | } 22 | 23 | this._recordingBuffer = new Array(this.numberOfChannels) 24 | .fill(new Float32Array(this.maxRecordingFrames)); 25 | 26 | this.recordedFrames = 0; 27 | this.isRecording = false; 28 | 29 | this.framesSinceLastPublish = 0; 30 | this.publishInterval = this.sampleRate * 5; 31 | 32 | this.port.onmessage = (event) => { 33 | if (event.data.message === 'UPDATE_RECORDING_STATE') { 34 | this.isRecording = event.data.setRecording; 35 | } 36 | }; 37 | } 38 | 39 | process(inputs, outputs) { 40 | for (let input = 0; input < 1; input++) { 41 | for (let channel = 0; channel < this.numberOfChannels; channel++) { 42 | for (let sample = 0; sample < inputs[input][channel].length; sample++) { 43 | const currentSample = inputs[input][channel][sample]; 44 | 45 | // Copy data to recording buffer. 46 | if (this.isRecording) { 47 | this._recordingBuffer[channel][sample+this.recordedFrames] = 48 | currentSample; 49 | } 50 | 51 | // Pass data directly to output, unchanged. 52 | outputs[input][channel][sample] = currentSample; 53 | 54 | } 55 | } 56 | } 57 | 58 | const shouldPublish = this.framesSinceLastPublish >= this.publishInterval; 59 | 60 | // Validate that recording hasn't reached its limit. 61 | if (this.isRecording) { 62 | if (this.recordedFrames + 128 < this.maxRecordingFrames) { 63 | this.recordedFrames += 128; 64 | 65 | // Post a recording recording length update on the clock's schedule 66 | if (shouldPublish) { 67 | 68 | this.port.postMessage({ 69 | message: 'SHARE_RECORDING_BUFFER', 70 | buffer: this._recordingBuffer, 71 | recordingLength: this.recordedFrames 72 | }); 73 | this.framesSinceLastPublish = 0; 74 | this.recordedFrames = 0 75 | } else { 76 | this.framesSinceLastPublish += 128; 77 | } 78 | } else { 79 | this.recordedFrames += 128; 80 | this.port.postMessage({ 81 | message: 'SHARE_RECORDING_BUFFER', 82 | buffer: this._recordingBuffer, 83 | recordingLength: this.recordedFrames 84 | }); 85 | 86 | this.recordedFrames = 0; 87 | this.framesSinceLastPublish = 0; 88 | 89 | } 90 | } else { 91 | console.log('stopping worklet processor node') 92 | this.recordedFrames = 0; 93 | this.framesSinceLastPublish = 0; 94 | return false; 95 | } 96 | 97 | return true; 98 | } 99 | } 100 | 101 | registerProcessor('audio-processor', AudioProcessor); 102 | -------------------------------------------------------------------------------- /lib/react-app/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /lib/react-app/src/App.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export default function App(): import("react").JSX.Element; 3 | -------------------------------------------------------------------------------- /lib/react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppLayout, 3 | Container, 4 | ContentLayout, 5 | Header, 6 | } from "@cloudscape-design/components"; 7 | import { Theme, applyTheme } from '@cloudscape-design/components/theming'; 8 | import { AwsCredentialsProvider } from "./context/AwsCredentialsContext"; 9 | import { SystemAudioProvider } from "./context/SystemAudioContext"; 10 | import TranscribeForm from "./components/TranscribeForm"; 11 | 12 | const theme: Theme = { 13 | tokens: { 14 | colorBackgroundLayoutMain: '#232F3E', 15 | colorTextHeadingDefault: '#ffffff' 16 | } 17 | }; 18 | applyTheme({ theme }); 19 | 20 | /** 21 | * The main entry point of the application. It sets up the necessary context providers, 22 | * applies a custom theme, and renders the main application layout with its content. 23 | * 24 | * @example 25 | * import App from './App'; 26 | * 27 | * const root = ReactDOM.createRoot(document.getElementById('root')); 28 | * root.render( 29 | * 30 | * 31 | * 32 | * ); 33 | */ 34 | export default function App() { 35 | return ( 36 | 37 | 38 | 46 | Knowledge Capture Demo with GenAI and Live Transcribe 47 | 48 | } 49 | > 50 | 51 | 52 | 53 | 54 | } 55 | /> 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /lib/react-app/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/genai-knowledge-capture-webapp/2707ab1c8bcdc39304c2229b579319ec78537788/lib/react-app/src/assets/favicon.ico -------------------------------------------------------------------------------- /lib/react-app/src/components/AudioPlayer.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@cloudscape-design/components"; 2 | import { useEffect, useRef, useState } from "react"; 3 | 4 | /** 5 | * The `AudioPlayer` component provides a reusable way to play and control audio recordings 6 | * in a React application. 7 | * 8 | * @component 9 | * @example 10 | * import AudioPlayer from './AudioPlayer'; 11 | * 12 | * {recordings.map((recording, index) => ( 13 | * 14 | * ))} 15 | * 16 | * @param {Object} props - The component's props. 17 | * @param {number} props.index - The index of the current audio recording. 18 | * @param {Blob} props.recording - The audio data to be played. 19 | * @param {boolean} props.isDisabled - Whether the audio player should be disabled. 20 | * @returns {JSX.Element} - The `AudioPlayer` component. 21 | */ 22 | function AudioPlayer({ 23 | index, 24 | recording, 25 | isDisabled, 26 | }: { 27 | index: number; 28 | recording: Blob; 29 | isDisabled: boolean; 30 | }) { 31 | const [isPlaying, setIsPlaying] = useState(false); 32 | const audioRef = useRef(null); 33 | 34 | const handleAudioClick = () => { 35 | if (audioRef.current) { 36 | if (isPlaying) { 37 | audioRef.current.pause(); 38 | audioRef.current.currentTime = 0; 39 | } else { 40 | audioRef.current.play(); 41 | } 42 | setIsPlaying((prevState) => !prevState); 43 | } 44 | }; 45 | 46 | useEffect(() => { 47 | if (audioRef.current) { 48 | audioRef.current.src = URL.createObjectURL(recording); 49 | 50 | const handleAudioEnded = () => { 51 | setIsPlaying(false); 52 | }; 53 | 54 | audioRef.current.addEventListener("ended", handleAudioEnded); 55 | const audioElement = audioRef.current; 56 | 57 | return () => { 58 | audioElement?.removeEventListener("ended", handleAudioEnded); 59 | }; 60 | } 61 | }, [recording]); 62 | 63 | return ( 64 |
65 |
75 | ); 76 | } 77 | 78 | export default AudioPlayer; 79 | -------------------------------------------------------------------------------- /lib/react-app/src/components/TranscribeForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useState } from "react"; 2 | import { 3 | Alert, 4 | Box, 5 | Button, 6 | FormField, 7 | Input, 8 | SpaceBetween, 9 | Spinner, 10 | Textarea, 11 | } from "@cloudscape-design/components"; 12 | import { SystemAudioContext } from "../context/SystemAudioContext"; 13 | import useAudioRecorder from "../hooks/useAudioRecorder"; 14 | import useAudioTranscription from "../hooks/useAudioTranscription"; 15 | import { callDocumentApi } from "../services/documentApi"; 16 | import AudioPlayer from "./AudioPlayer"; 17 | 18 | /** 19 | * Provides the input form for the document summarization and generation functionality. 20 | * It captures audio transcriptions using Amazon Transcribe's streaming transcription 21 | * capabilities. It uses the SystemAudioContext context to get the user's system audio 22 | * stream, uses the useAudioTranscription hook to manage the audio transcription process, 23 | * and uses the useAudioRecorder hook to manage audio recording. It displays the 24 | * transcribed text for editing, and provides a form for submitting the transcribed text 25 | * to the backend API for document generation. 26 | * 27 | * @component 28 | * @example 29 | * import TranscribeForm from './TranscribeForm'; 30 | * 31 | * const App = () => { 32 | * return ( 33 | * 34 | * 35 | * 36 | * ); 37 | * }; 38 | */ 39 | const TranscribeForm: React.FC = () => { 40 | const [question, setQuestion] = useState(""); 41 | const [answer, setAnswer] = useState(""); 42 | const [filename, setFilename] = useState(""); 43 | const [recordings, setRecordings] = useState([]); 44 | const [partialTranscribeResponse, setPartialTranscribeResponse] = 45 | useState(""); 46 | const [documentLink, setDocumentLink] = useState(null); 47 | const [audioLinks, setAudioLinks] = useState([]); 48 | const [isGeneratingDocument, setIsGeneratingDocument] = useState(false); 49 | const [alert, setAlert] = useState(false); 50 | 51 | const { audioStream } = React.useContext(SystemAudioContext); 52 | const [ 53 | isRecordingReady, 54 | isRecording, 55 | startRecording, 56 | stopRecording, 57 | audioBlobs, 58 | resetAudioBlobs, 59 | ] = useAudioRecorder(audioStream); 60 | const [ 61 | isTranscriptionReady, 62 | isTranscribing, 63 | startTranscription, 64 | stopTranscription, 65 | transcribeResponse, 66 | resetTranscribeResponse, 67 | ] = useAudioTranscription(audioStream); 68 | 69 | // Handle Start Transcription / Stop Transcription button click 70 | const handleStartStop = async () => { 71 | if (!isTranscribing) { 72 | // Clear links to previously created artifacts 73 | setAudioLinks([]); 74 | setDocumentLink(null); 75 | // Start recording and transcription 76 | await startRecording(); 77 | await startTranscription(); 78 | } else { 79 | // Stop recording and transcription 80 | await stopRecording(); 81 | await stopTranscription(); 82 | // Capture any partial transcription remaining 83 | if (transcribeResponse) { 84 | appendAnswer(transcribeResponse.text); 85 | } 86 | } 87 | }; 88 | 89 | // Handle manual editing of transcribed answer 90 | const handleManualAnswerChange = (newAnswer: string) => { 91 | setAnswer(newAnswer); 92 | }; 93 | 94 | // Handle Generate Document button click 95 | const generateDocument = async () => { 96 | setIsGeneratingDocument(true); 97 | setDocumentLink(null); 98 | setAudioLinks([]); 99 | 100 | const response = await callDocumentApi( 101 | filename, 102 | question, 103 | answer, 104 | recordings 105 | ); 106 | if (response?.pdfFileS3Uri && response?.audioS3Uris) { 107 | setDocumentLink(response.pdfFileS3Uri); 108 | setAudioLinks(response.audioS3Uris); 109 | } else { 110 | setAlert(true); 111 | } 112 | setIsGeneratingDocument(false); 113 | }; 114 | 115 | // Append transcription text to user's answer 116 | const appendAnswer = useCallback( 117 | (text: string) => { 118 | console.log("Transcription captured:", text); 119 | setAnswer(`${answer} ${text}`.trim()); 120 | setPartialTranscribeResponse(""); 121 | resetTranscribeResponse(); 122 | }, 123 | [answer, resetTranscribeResponse] 124 | ); 125 | 126 | // When an audio recording is complete, add it to the existing recording 127 | useEffect(() => { 128 | if (audioBlobs.length > 0) { 129 | console.log("Recording captured:", audioBlobs); 130 | setRecordings((prev) => [...prev, ...audioBlobs]); 131 | resetAudioBlobs(); 132 | } 133 | }, [audioBlobs, recordings, resetAudioBlobs]); 134 | 135 | // When new transcription is complete, append it to the existing answer text 136 | useEffect(() => { 137 | if (transcribeResponse) { 138 | if (transcribeResponse.partial) { 139 | setPartialTranscribeResponse(transcribeResponse.text); 140 | } else { 141 | appendAnswer(transcribeResponse.text); 142 | } 143 | } 144 | }, [appendAnswer, transcribeResponse]); 145 | 146 | return ( 147 | 148 | 149 | {alert && ( 150 | setAlert(false)} 156 | > 157 | )} 158 | 162 | setQuestion(event.detail.value)} 166 | disabled={isGeneratingDocument || isTranscribing} 167 | /> 168 | 169 | 170 | 175 |