├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── architecture.png ├── backend ├── .gitignore ├── __init__.py ├── src │ ├── add_conversation │ │ ├── __init__.py │ │ ├── main.py │ │ └── requirements.txt │ ├── delete_document │ │ ├── __init__.py │ │ ├── main.py │ │ └── requirements.txt │ ├── generate_embeddings │ │ ├── __init__.py │ │ ├── main.py │ │ └── requirements.txt │ ├── generate_presigned_url │ │ ├── __init__.py │ │ ├── main.py │ │ └── requirements.txt │ ├── generate_response │ │ ├── __init__.py │ │ ├── main.py │ │ └── requirements.txt │ ├── get_all_documents │ │ ├── __init__.py │ │ └── main.py │ ├── get_document │ │ ├── __init__.py │ │ └── main.py │ └── upload_trigger │ │ ├── __init__.py │ │ ├── main.py │ │ └── requirements.txt └── template.yaml ├── frontend ├── .eslintrc.cjs ├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── github.svg │ ├── loading-dots.svg │ └── loading-grid.svg ├── src │ ├── App.tsx │ ├── common │ │ ├── types.ts │ │ └── utilities.ts │ ├── components │ │ ├── ChatMessages.tsx │ │ ├── ChatSidebar.tsx │ │ ├── DocumentDetail.tsx │ │ ├── DocumentList.tsx │ │ ├── DocumentUploader.tsx │ │ ├── Footer.tsx │ │ └── Navigation.tsx │ ├── index.css │ ├── main.tsx │ ├── routes │ │ ├── chat.tsx │ │ ├── documents.tsx │ │ └── layout.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── preview-1.png └── preview-2.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### SAM ### 5 | .aws-sam/ 6 | samconfig.toml 7 | 8 | ### Linux ### 9 | *~ 10 | 11 | # temporary files which can be created if a process still has a handle open of a deleted file 12 | .fuse_hidden* 13 | 14 | # KDE directory preferences 15 | .directory 16 | 17 | # Linux trash folder which might appear on any partition or disk 18 | .Trash-* 19 | 20 | # .nfs files are created when an open file is removed but is still being accessed 21 | .nfs* 22 | 23 | ### OSX ### 24 | *.DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### PyCharm ### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 52 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 53 | 54 | # User-specific stuff: 55 | .idea/**/workspace.xml 56 | .idea/**/tasks.xml 57 | .idea/dictionaries 58 | 59 | # Sensitive or high-churn files: 60 | .idea/**/dataSources/ 61 | .idea/**/dataSources.ids 62 | .idea/**/dataSources.xml 63 | .idea/**/dataSources.local.xml 64 | .idea/**/sqlDataSources.xml 65 | .idea/**/dynamic.xml 66 | .idea/**/uiDesigner.xml 67 | 68 | # Gradle: 69 | .idea/**/gradle.xml 70 | .idea/**/libraries 71 | 72 | # CMake 73 | cmake-build-debug/ 74 | 75 | # Mongo Explorer plugin: 76 | .idea/**/mongoSettings.xml 77 | 78 | ## File-based project format: 79 | *.iws 80 | 81 | ## Plugin-specific files: 82 | 83 | # IntelliJ 84 | /out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Cursive Clojure plugin 93 | .idea/replstate.xml 94 | 95 | # Ruby plugin and RubyMine 96 | /.rakeTasks 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | fabric.properties 103 | 104 | ### PyCharm Patch ### 105 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 106 | 107 | # *.iml 108 | # modules.xml 109 | # .idea/misc.xml 110 | # *.ipr 111 | 112 | # Sonarlint plugin 113 | .idea/sonarlint 114 | 115 | ### Python ### 116 | # Byte-compiled / optimized / DLL files 117 | __pycache__/ 118 | *.py[cod] 119 | *$py.class 120 | 121 | # C extensions 122 | *.so 123 | 124 | # Distribution / packaging 125 | .Python 126 | build/ 127 | develop-eggs/ 128 | dist/ 129 | downloads/ 130 | eggs/ 131 | .eggs/ 132 | lib/ 133 | lib64/ 134 | parts/ 135 | sdist/ 136 | var/ 137 | wheels/ 138 | *.egg-info/ 139 | .installed.cfg 140 | *.egg 141 | 142 | # PyInstaller 143 | # Usually these files are written by a python script from a template 144 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 145 | *.manifest 146 | *.spec 147 | 148 | # Installer logs 149 | pip-log.txt 150 | pip-delete-this-directory.txt 151 | 152 | # Unit test / coverage reports 153 | htmlcov/ 154 | .tox/ 155 | .coverage 156 | .coverage.* 157 | .cache 158 | .pytest_cache/ 159 | nosetests.xml 160 | coverage.xml 161 | *.cover 162 | .hypothesis/ 163 | 164 | # Translations 165 | *.mo 166 | *.pot 167 | 168 | # Flask stuff: 169 | instance/ 170 | .webassets-cache 171 | 172 | # Scrapy stuff: 173 | .scrapy 174 | 175 | # Sphinx documentation 176 | docs/_build/ 177 | 178 | # PyBuilder 179 | target/ 180 | 181 | # Jupyter Notebook 182 | .ipynb_checkpoints 183 | 184 | # pyenv 185 | .python-version 186 | 187 | # celery beat schedule file 188 | celerybeat-schedule.* 189 | 190 | # SageMath parsed files 191 | *.sage.py 192 | 193 | # Environments 194 | .env 195 | .venv 196 | env/ 197 | venv/ 198 | ENV/ 199 | env.bak/ 200 | venv.bak/ 201 | 202 | # Spyder project settings 203 | .spyderproject 204 | .spyproject 205 | 206 | # Rope project settings 207 | .ropeproject 208 | 209 | # mkdocs documentation 210 | /site 211 | 212 | # mypy 213 | .mypy_cache/ 214 | 215 | ### VisualStudioCode ### 216 | .vscode 217 | .vscode/* 218 | !.vscode/settings.json 219 | !.vscode/tasks.json 220 | !.vscode/launch.json 221 | !.vscode/extensions.json 222 | .history 223 | 224 | ### Windows ### 225 | # Windows thumbnail cache files 226 | Thumbs.db 227 | ehthumbs.db 228 | ehthumbs_vista.db 229 | 230 | # Folder config file 231 | Desktop.ini 232 | 233 | # Recycle Bin used on file shares 234 | $RECYCLE.BIN/ 235 | 236 | # Windows Installer files 237 | *.cab 238 | *.msi 239 | *.msm 240 | *.msp 241 | 242 | # Windows shortcuts 243 | *.lnk 244 | 245 | # Build folder 246 | 247 | */build/* 248 | 249 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 250 | 251 | events/*.json 252 | dependencies -------------------------------------------------------------------------------- /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 | # Serverless document chat application 2 | 3 | This sample application allows you to ask natural language questions of any PDF document you upload. It combines the text generation and analysis capabilities of an LLM with a vector search of the document content. The solution uses serverless services such as [Amazon Bedrock](https://aws.amazon.com/bedrock/) to access foundational models, [AWS Lambda](https://aws.amazon.com/lambda/) to run [LangChain](https://github.com/langchain-ai/langchain), and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) for conversational memory. 4 | 5 | See the [accompanying blog post on the AWS Serverless Blog](https://aws.amazon.com/blogs/compute/building-a-serverless-document-chat-with-aws-lambda-and-amazon-bedrock/) for a detailed description and follow the deployment instructions below to get started. 6 | 7 |

8 | 9 | 10 |

11 | 12 | > **Warning** 13 | > This application is not ready for production use. It was written for demonstration and educational purposes. Review the [Security](#security) section of this README and consult with your security team before deploying this stack. No warranty is implied in this example. 14 | 15 | > **Note** 16 | > This architecture creates resources that have costs associated with them. Please see the [AWS Pricing](https://aws.amazon.com/pricing/) page for details and make sure to understand the costs before deploying this stack. 17 | 18 | ## Key features 19 | 20 | - [Amazon Bedrock](https://aws.amazon.com/de/bedrock/) for serverless embedding and inference 21 | - [LangChain](https://github.com/hwchase17/langchain) to orchestrate a Q&A LLM chain 22 | - [FAISS](https://github.com/facebookresearch/faiss) vector store 23 | - [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) for serverless conversational memory 24 | - [AWS Lambda](https://aws.amazon.com/lambda/) for serverless compute 25 | - Frontend built in [React](https://react.dev/), [TypeScript](https://www.typescriptlang.org/), [TailwindCSS](https://tailwindcss.com/), and [Vite](https://vitejs.dev/). 26 | - Run locally or deploy to [AWS Amplify Hosting](https://aws.amazon.com/amplify/hosting/) 27 | - [Amazon Cognito](https://aws.amazon.com/cognito/) for authentication 28 | 29 | ## How the application works 30 | 31 | ![Serverless PDF Chat architecture](architecture.png "Serverless PDF Chat architecture") 32 | 33 | 1. A user uploads a PDF document into an [Amazon Simple Storage Service](https://aws.amazon.com/s3/) (S3) bucket through a static web application frontend. 34 | 1. This upload triggers a metadata extraction and document embedding process. The process converts the text in the document into vectors. The vectors are loaded into a vector index and stored in S3 for later use. 35 | 1. When a user chats with a PDF document and sends a prompt to the backend, a Lambda function retrieves the index from S3 and searches for information related to the prompt. 36 | 1. A LLM then uses the results of this vector search, previous messages in the conversation, and its general-purpose capabilities to formulate a response to the user. 37 | 38 | ## Deployment instructions 39 | 40 | ### Prerequisites 41 | 42 | - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) 43 | - [Python](https://www.python.org/) 3.11 or greater 44 | 45 | ### Cloning the repository 46 | 47 | Clone this repository: 48 | 49 | ```bash 50 | git clone https://github.com/aws-samples/serverless-pdf-chat.git 51 | ``` 52 | 53 | ### Amazon Bedrock setup 54 | 55 | This application can be used with a variety of Amazon Bedrock models. See [Supported models in Amazon Bedrock](https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-service.html#models-supported) for a complete list. 56 | 57 | By default, this application uses **Titan Embeddings G1 - Text** to generate embeddings and **Anthropic Claude v3 Sonnet** for responses. 58 | 59 | > **Important -** 60 | > Before you can use these models with this application, **you must request access in the Amazon Bedrock console**. See the [Model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) section of the Bedrock User Guide for detailed instructions. 61 | > By default, this application is configured to use Amazon Bedrock in the `us-east-1` Region, make sure you request model access in that Region (this does not have to be the same Region that you deploy this stack to). 62 | 63 | To select your Bedrock model, specify the `ModelId` parameter during the AWS SAM deployment, such as `anthropic.claude-3-sonnet-20240229-v1:0`. See [Amazon Bedrock model IDs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html) for a complete list. 64 | 65 | The `ModelId` parameter is used in the GenerateResponseFunction Lambda function of your AWS SAM template to instantiate [LangChain BedrockChat](https://js.langchain.com/v0.1/docs/integrations/chat/bedrock/) and [ConversationalRetrievalChain](https://api.python.langchain.com/en/latest/chains/langchain.chains.conversational_retrieval.base.ConversationalRetrievalChain.html) objects, providing efficient retrieval of relevant context from large PDF datasets to enable the Bedrock model-generated response. 66 | 67 | ```python 68 | def bedrock_chain(faiss_index, memory, human_input, bedrock_runtime): 69 | 70 | chat = BedrockChat( 71 | model_id=MODEL_ID, 72 | model_kwargs={'temperature': 0.0} 73 | ) 74 | 75 | chain = ConversationalRetrievalChain.from_llm( 76 | llm=chat, 77 | chain_type="stuff", 78 | retriever=faiss_index.as_retriever(), 79 | memory=memory, 80 | return_source_documents=True, 81 | ) 82 | 83 | response = chain.invoke({"question": human_input}) 84 | 85 | return response 86 | ``` 87 | 88 | ### Deploy the frontend with AWS Amplify Hosting 89 | 90 | [AWS Amplify Hosting](https://aws.amazon.com/amplify/hosting/) enables a fully-managed deployment of the application's React frontend in an AWS-managed account using Amazon S3 and [Amazon CloudFront](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html). You can optionally run the React frontend locally by skipping to [Deploy the application with AWS SAM](#Deploy-the-application-with-AWS-SAM). 91 | 92 | To set up Amplify Hosting: 93 | 94 | 1. Fork this GitHub repository and take note of your repository URL, for example `https://github.com/user/serverless-pdf-chat/`. 95 | 1. Create a GitHub fine-grained access token for the new repository by following [this guide](https://docs.aws.amazon.com/amplify/latest/userguide/setting-up-GitHub-access.html). For the **Repository permissions**, select **Read and write** for **Content** and **Webhooks**. 96 | 1. Create a new secret called `serverless-pdf-chat-github-token` in AWS Secrets Manager and input your fine-grained access token as plaintext. Select the **Plaintext** tab and confirm your secret looks like this: 97 | 98 | ```json 99 | github_pat_T2wyo------------------------------------------------------------------------rs0Pp 100 | ``` 101 | 102 | ### Deploy the application with AWS SAM 103 | 104 | 1. Change to the `backend` directory and [build](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html) the application: 105 | 106 | ```bash 107 | cd backend 108 | sam build 109 | ``` 110 | 111 | 1. [Deploy](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html) the application into your AWS account: 112 | 113 | ```bash 114 | sam deploy --guided 115 | ``` 116 | 117 | 1. For **Stack Name**, choose `serverless-pdf-chat`. 118 | 119 | 1. For **Frontend**, specify the environment ("local", "amplify") for the frontend of the application. 120 | 121 | 1. If you selected "amplify", specify the URL of the forked Git repository containing the application code. 122 | 123 | 1. Specify the Amazon Bedrock model ID. For example, `anthropic.claude-3-sonnet-20240229-v1:0`. 124 | 125 | 1. For the remaining options, keep the defaults by pressing the enter key. 126 | 127 | AWS SAM will now provision the AWS resources defined in the `backend/template.yaml` template. Once the deployment is completed successfully, you will see a set of output values similar to the following: 128 | 129 | ```bash 130 | CloudFormation outputs from deployed stack 131 | ------------------------------------------------------------------------------- 132 | Outputs 133 | ------------------------------------------------------------------------------- 134 | Key CognitoUserPool 135 | Description - 136 | Value us-east-1_gxKtRocFs 137 | 138 | Key CognitoUserPoolClient 139 | Description - 140 | Value 874ghcej99f8iuo0lgdpbrmi76k 141 | 142 | Key ApiGatewayBaseUrl 143 | Description - 144 | Value https://abcd1234.execute-api.us-east-1.amazonaws.com/dev/ 145 | ------------------------------------------------------------------------------- 146 | ``` 147 | 148 | If you selected to deploy the React frontend using Amplify Hosting, navigate to the Amplify console to check the build status. If the build does not start automatically, trigger it through the Amplify console. 149 | 150 | If you selected to run the React frontend locally and connect to the deployed resources in AWS, you will use the CloudFormation stack outputs in the following section. 151 | 152 | ### Optional: Run the React frontend locally 153 | 154 | Create a file named `.env.development` in the `frontend` directory. [Vite will use this file](https://vitejs.dev/guide/env-and-mode.html) to set up environment variables when we run the application locally. 155 | 156 | Copy the following file content and replace the values with the outputs provided by AWS SAM: 157 | 158 | ```plaintext 159 | VITE_REGION=us-east-1 160 | VITE_API_ENDPOINT=https://abcd1234.execute-api.us-east-1.amazonaws.com/dev/ 161 | VITE_USER_POOL_ID=us-east-1_gxKtRocFs 162 | VITE_USER_POOL_CLIENT_ID=874ghcej99f8iuo0lgdpbrmi76k 163 | ``` 164 | 165 | Next, install the frontend's dependencies by running the following command in the `frontend` directory: 166 | 167 | ```bash 168 | npm ci 169 | ``` 170 | 171 | Finally, to start the application locally, run the following command in the `frontend` directory: 172 | 173 | ```bash 174 | npm run dev 175 | ``` 176 | 177 | Vite will now start the application under `http://localhost:5173`. 178 | 179 | ### Create a user in the Amazon Cognito user pool 180 | 181 | The application uses Amazon Cognito to authenticate users through a login screen. In this step, you will create a user to access the application. 182 | 183 | Perform the following steps to create a user in the Cognito user pool: 184 | 185 | 1. Navigate to the **Amazon Cognito console**. 186 | 1. Find the user pool with an ID matching the output provided by AWS SAM above. 187 | 1. Under Users, choose **Create user**. 188 | 1. Enter an email address and a password that adheres to the password requirements. 189 | 1. Choose **Create user**. 190 | 191 | Navigate back to your Amplify website URL or local host address to log in with the new user's credentials. 192 | 193 | ## Cleanup 194 | 195 | 1. Delete any secrets in AWS Secrets Manager created as part of this walkthrough. 196 | 1. [Empty the Amazon S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/empty-bucket.html) created as part of the AWS SAM template. 197 | 1. Run the following command in the `backend` directory of the project to delete all associated resources resources: 198 | 199 | ```bash 200 | sam delete 201 | ``` 202 | ## Troubleshooting 203 | 204 | If you are experiencing issues when running the [`sam build`](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-build.html) command, try setting the `--use-container` flag (requires Docker): 205 | 206 | ```bash 207 | sam build --use-container 208 | ``` 209 | 210 | If you are still experiencing issues despite using `--use-container`, try switching the AWS Lambda functions from `arm64` to `x86_64` in the `backend/template.yaml` (as well as switching to the `x_86_64` version of Powertools): 211 | 212 | ```yaml 213 | Globals: 214 | Function: 215 | Runtime: python3.11 216 | Handler: main.lambda_handler 217 | Architectures: 218 | - x86_64 219 | Tracing: Active 220 | Environment: 221 | Variables: 222 | LOG_LEVEL: INFO 223 | Layers: 224 | - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV2:51 225 | ``` 226 | 227 | ## Security 228 | 229 | This application was written for demonstration and educational purposes and not for production use. The [Security Pillar of the AWS Well-Architected Framework](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html) can support you in further adopting the sample into a production deployment in addition to your own established processes. Take note of the following: 230 | 231 | - The application uses encryption in transit and at rest with AWS-managed keys where applicable. Optionally, use [AWS KMS](https://aws.amazon.com/kms/) with [DynamoDB](https://docs.aws.amazon.com/kms/latest/developerguide/services-dynamodb.html), [SQS](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-server-side-encryption.html), and [S3](https://docs.aws.amazon.com/kms/latest/developerguide/services-s3.html) for more control over encryption keys. 232 | 233 | - This application uses [Powertools for AWS Lambda (Python)](https://github.com/aws-powertools/powertools-lambda-python) to log to inputs and ouputs to CloudWatch Logs. Per default, this can include sensitive data contained in user input. Adjust the log level and remove log statements to fit your security requirements. 234 | 235 | - [API Gateway access logging](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html#set-up-access-logging-using-console) and [usage plans](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-usage-plans.html) are not activiated in this code sample. Similarly, [S3 access logging](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-loggingconfig.html) is currently not enabled. 236 | 237 | - In order to simplify the setup of the demo, this solution uses AWS managed policies associated to IAM roles that contain wildcards on resources. Please consider to further scope down the policies as you see fit according to your needs. Please note that there is a resource wildcard on the AWS managed `AWSLambdaSQSQueueExecutionRole`. This is a known behaviour, see [this GitHub issue](https://github.com/aws/serverless-application-model/issues/2118) for details. 238 | 239 | - If your security controls require inspecting network traffic, consider [adjusting the AWS SAM template](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html) to attach the Lambda functions to a VPC via its [`VpcConfig`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-vpcconfig.html). 240 | 241 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 242 | 243 | ## License 244 | 245 | This library is licensed under the MIT-0 License. See the [LICENSE](LICENSE) file. 246 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/architecture.png -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### SAM ### 5 | .aws-sam/ 6 | samconfig.toml 7 | 8 | ### Linux ### 9 | *~ 10 | 11 | # temporary files which can be created if a process still has a handle open of a deleted file 12 | .fuse_hidden* 13 | 14 | # KDE directory preferences 15 | .directory 16 | 17 | # Linux trash folder which might appear on any partition or disk 18 | .Trash-* 19 | 20 | # .nfs files are created when an open file is removed but is still being accessed 21 | .nfs* 22 | 23 | ### OSX ### 24 | *.DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | .com.apple.timemachine.donotpresent 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | 50 | ### PyCharm ### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 52 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 53 | 54 | # User-specific stuff: 55 | .idea/**/workspace.xml 56 | .idea/**/tasks.xml 57 | .idea/dictionaries 58 | 59 | # Sensitive or high-churn files: 60 | .idea/**/dataSources/ 61 | .idea/**/dataSources.ids 62 | .idea/**/dataSources.xml 63 | .idea/**/dataSources.local.xml 64 | .idea/**/sqlDataSources.xml 65 | .idea/**/dynamic.xml 66 | .idea/**/uiDesigner.xml 67 | 68 | # Gradle: 69 | .idea/**/gradle.xml 70 | .idea/**/libraries 71 | 72 | # CMake 73 | cmake-build-debug/ 74 | 75 | # Mongo Explorer plugin: 76 | .idea/**/mongoSettings.xml 77 | 78 | ## File-based project format: 79 | *.iws 80 | 81 | ## Plugin-specific files: 82 | 83 | # IntelliJ 84 | /out/ 85 | 86 | # mpeltonen/sbt-idea plugin 87 | .idea_modules/ 88 | 89 | # JIRA plugin 90 | atlassian-ide-plugin.xml 91 | 92 | # Cursive Clojure plugin 93 | .idea/replstate.xml 94 | 95 | # Ruby plugin and RubyMine 96 | /.rakeTasks 97 | 98 | # Crashlytics plugin (for Android Studio and IntelliJ) 99 | com_crashlytics_export_strings.xml 100 | crashlytics.properties 101 | crashlytics-build.properties 102 | fabric.properties 103 | 104 | ### PyCharm Patch ### 105 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 106 | 107 | # *.iml 108 | # modules.xml 109 | # .idea/misc.xml 110 | # *.ipr 111 | 112 | # Sonarlint plugin 113 | .idea/sonarlint 114 | 115 | ### Python ### 116 | # Byte-compiled / optimized / DLL files 117 | __pycache__/ 118 | *.py[cod] 119 | *$py.class 120 | 121 | # C extensions 122 | *.so 123 | 124 | # Distribution / packaging 125 | .Python 126 | build/ 127 | develop-eggs/ 128 | dist/ 129 | downloads/ 130 | eggs/ 131 | .eggs/ 132 | lib/ 133 | lib64/ 134 | parts/ 135 | sdist/ 136 | var/ 137 | wheels/ 138 | *.egg-info/ 139 | .installed.cfg 140 | *.egg 141 | 142 | # PyInstaller 143 | # Usually these files are written by a python script from a template 144 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 145 | *.manifest 146 | *.spec 147 | 148 | # Installer logs 149 | pip-log.txt 150 | pip-delete-this-directory.txt 151 | 152 | # Unit test / coverage reports 153 | htmlcov/ 154 | .tox/ 155 | .coverage 156 | .coverage.* 157 | .cache 158 | .pytest_cache/ 159 | nosetests.xml 160 | coverage.xml 161 | *.cover 162 | .hypothesis/ 163 | 164 | # Translations 165 | *.mo 166 | *.pot 167 | 168 | # Flask stuff: 169 | instance/ 170 | .webassets-cache 171 | 172 | # Scrapy stuff: 173 | .scrapy 174 | 175 | # Sphinx documentation 176 | docs/_build/ 177 | 178 | # PyBuilder 179 | target/ 180 | 181 | # Jupyter Notebook 182 | .ipynb_checkpoints 183 | 184 | # pyenv 185 | .python-version 186 | 187 | # celery beat schedule file 188 | celerybeat-schedule.* 189 | 190 | # SageMath parsed files 191 | *.sage.py 192 | 193 | # Environments 194 | .env 195 | .venv 196 | env/ 197 | venv/ 198 | ENV/ 199 | env.bak/ 200 | venv.bak/ 201 | 202 | # Spyder project settings 203 | .spyderproject 204 | .spyproject 205 | 206 | # Rope project settings 207 | .ropeproject 208 | 209 | # mkdocs documentation 210 | /site 211 | 212 | # mypy 213 | .mypy_cache/ 214 | 215 | ### VisualStudioCode ### 216 | .vscode 217 | .vscode/* 218 | !.vscode/settings.json 219 | !.vscode/tasks.json 220 | !.vscode/launch.json 221 | !.vscode/extensions.json 222 | .history 223 | 224 | ### Windows ### 225 | # Windows thumbnail cache files 226 | Thumbs.db 227 | ehthumbs.db 228 | ehthumbs_vista.db 229 | 230 | # Folder config file 231 | Desktop.ini 232 | 233 | # Recycle Bin used on file shares 234 | $RECYCLE.BIN/ 235 | 236 | # Windows Installer files 237 | *.cab 238 | *.msi 239 | *.msm 240 | *.msp 241 | 242 | # Windows shortcuts 243 | *.lnk 244 | 245 | # Build folder 246 | 247 | */build/* 248 | 249 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode -------------------------------------------------------------------------------- /backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/__init__.py -------------------------------------------------------------------------------- /backend/src/add_conversation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/add_conversation/__init__.py -------------------------------------------------------------------------------- /backend/src/add_conversation/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | from datetime import datetime 3 | import boto3 4 | import shortuuid 5 | from aws_lambda_powertools import Logger 6 | 7 | DOCUMENT_TABLE = os.environ["DOCUMENT_TABLE"] 8 | MEMORY_TABLE = os.environ["MEMORY_TABLE"] 9 | 10 | 11 | ddb = boto3.resource("dynamodb") 12 | document_table = ddb.Table(DOCUMENT_TABLE) 13 | memory_table = ddb.Table(MEMORY_TABLE) 14 | logger = Logger() 15 | 16 | 17 | @logger.inject_lambda_context(log_event=True) 18 | def lambda_handler(event, context): 19 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 20 | document_id = event["pathParameters"]["documentid"] 21 | 22 | response = document_table.get_item( 23 | Key={"userid": user_id, "documentid": document_id} 24 | ) 25 | conversations = response["Item"]["conversations"] 26 | logger.info({"conversations": conversations}) 27 | 28 | conversation_id = shortuuid.uuid() 29 | timestamp = datetime.utcnow() 30 | timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") 31 | conversation = { 32 | "conversationid": conversation_id, 33 | "created": timestamp_str, 34 | } 35 | conversations.append(conversation) 36 | logger.info({"conversation_new": conversation}) 37 | document_table.update_item( 38 | Key={"userid": user_id, "documentid": document_id}, 39 | UpdateExpression="SET conversations = :conversations", 40 | ExpressionAttributeValues={":conversations": conversations}, 41 | ) 42 | 43 | conversation = {"SessionId": conversation_id, "History": []} 44 | memory_table.put_item(Item=conversation) 45 | 46 | return { 47 | "statusCode": 200, 48 | "headers": { 49 | "Content-Type": "application/json", 50 | "Access-Control-Allow-Headers": "*", 51 | "Access-Control-Allow-Origin": "*", 52 | "Access-Control-Allow-Methods": "*", 53 | }, 54 | "body": json.dumps({"conversationid": conversation_id}), 55 | } 56 | -------------------------------------------------------------------------------- /backend/src/add_conversation/requirements.txt: -------------------------------------------------------------------------------- 1 | shortuuid==1.0.11 -------------------------------------------------------------------------------- /backend/src/delete_document/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/delete_document/__init__.py -------------------------------------------------------------------------------- /backend/src/delete_document/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | import boto3 3 | from aws_lambda_powertools import Logger 4 | 5 | 6 | DOCUMENT_TABLE = os.environ["DOCUMENT_TABLE"] 7 | MEMORY_TABLE = os.environ["MEMORY_TABLE"] 8 | BUCKET = os.environ["BUCKET"] 9 | 10 | ddb = boto3.resource("dynamodb") 11 | document_table = ddb.Table(DOCUMENT_TABLE) 12 | memory_table = ddb.Table(MEMORY_TABLE) 13 | s3 = boto3.client("s3") 14 | logger = Logger() 15 | 16 | 17 | @logger.inject_lambda_context(log_event=True) 18 | def lambda_handler(event, context): 19 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 20 | document_id = event["pathParameters"]["documentid"] 21 | 22 | response = document_table.get_item( 23 | Key={"userid": user_id, "documentid": document_id} 24 | ) 25 | document = response["Item"] 26 | logger.info({"document": document}) 27 | logger.info("Deleting DDB items") 28 | with memory_table.batch_writer() as batch: 29 | for item in document["conversations"]: 30 | batch.delete_item(Key={"SessionId": item["conversationid"]}) 31 | 32 | document_table.delete_item( 33 | Key={"userid": user_id, "documentid": document_id} 34 | ) 35 | 36 | logger.info("Deleting S3 objects") 37 | filename = document["filename"] 38 | objects = [{"Key": f"{user_id}/{filename}/{key}"} for key in [filename, "index.faiss", "index.pkl"]] 39 | response = s3.delete_objects( 40 | Bucket=BUCKET, 41 | Delete={ 42 | "Objects": objects, 43 | "Quiet": True, 44 | }, 45 | ) 46 | logger.info({"Response": response}) 47 | 48 | return { 49 | "statusCode": 200, 50 | "headers": { 51 | "Content-Type": "application/json", 52 | "Access-Control-Allow-Headers": "*", 53 | "Access-Control-Allow-Origin": "*", 54 | "Access-Control-Allow-Methods": "*", 55 | }, 56 | "body": json.dumps( 57 | {}, 58 | default=str, 59 | ), 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/delete_document/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.28.57 2 | botocore==1.31.57 3 | -------------------------------------------------------------------------------- /backend/src/generate_embeddings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/generate_embeddings/__init__.py -------------------------------------------------------------------------------- /backend/src/generate_embeddings/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | import boto3 3 | from aws_lambda_powertools import Logger 4 | from langchain.indexes import VectorstoreIndexCreator 5 | from langchain_aws.embeddings import BedrockEmbeddings 6 | from langchain_community.document_loaders import PyPDFLoader 7 | from langchain_community.vectorstores import FAISS 8 | 9 | 10 | DOCUMENT_TABLE = os.environ["DOCUMENT_TABLE"] 11 | BUCKET = os.environ["BUCKET"] 12 | EMBEDDING_MODEL_ID = os.environ["EMBEDDING_MODEL_ID"] 13 | 14 | s3 = boto3.client("s3") 15 | ddb = boto3.resource("dynamodb") 16 | document_table = ddb.Table(DOCUMENT_TABLE) 17 | logger = Logger() 18 | 19 | 20 | def set_doc_status(user_id, document_id, status): 21 | document_table.update_item( 22 | Key={"userid": user_id, "documentid": document_id}, 23 | UpdateExpression="SET docstatus = :docstatus", 24 | ExpressionAttributeValues={":docstatus": status}, 25 | ) 26 | 27 | 28 | @logger.inject_lambda_context(log_event=True) 29 | def lambda_handler(event, context): 30 | event_body = json.loads(event["Records"][0]["body"]) 31 | document_id = event_body["documentid"] 32 | user_id = event_body["user"] 33 | key = event_body["key"] 34 | file_name_full = key.split("/")[-1] 35 | 36 | set_doc_status(user_id, document_id, "PROCESSING") 37 | 38 | s3.download_file(BUCKET, key, f"/tmp/{file_name_full}") 39 | 40 | loader = PyPDFLoader(f"/tmp/{file_name_full}") 41 | 42 | bedrock_runtime = boto3.client( 43 | service_name="bedrock-runtime", 44 | region_name="us-east-1", 45 | ) 46 | 47 | embeddings = BedrockEmbeddings( 48 | model_id=EMBEDDING_MODEL_ID, 49 | client=bedrock_runtime, 50 | region_name="us-east-1", 51 | ) 52 | 53 | index_creator = VectorstoreIndexCreator( 54 | vectorstore_cls=FAISS, 55 | embedding=embeddings, 56 | ) 57 | 58 | index_from_loader = index_creator.from_loaders([loader]) 59 | 60 | index_from_loader.vectorstore.save_local("/tmp") 61 | 62 | s3.upload_file( 63 | "/tmp/index.faiss", BUCKET, f"{user_id}/{file_name_full}/index.faiss" 64 | ) 65 | s3.upload_file("/tmp/index.pkl", BUCKET, f"{user_id}/{file_name_full}/index.pkl") 66 | 67 | set_doc_status(user_id, document_id, "READY") 68 | -------------------------------------------------------------------------------- /backend/src/generate_embeddings/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | botocore 3 | faiss-cpu==1.7.4 4 | langchain==0.3.21 5 | langchain-community==0.3.20 6 | langchain-aws==0.2.17 7 | pypdf==3.17.0 8 | urllib3 -------------------------------------------------------------------------------- /backend/src/generate_presigned_url/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/generate_presigned_url/__init__.py -------------------------------------------------------------------------------- /backend/src/generate_presigned_url/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | import boto3 3 | from botocore.config import Config 4 | import shortuuid 5 | from aws_lambda_powertools import Logger 6 | 7 | 8 | BUCKET = os.environ["BUCKET"] 9 | REGION = os.environ["REGION"] 10 | 11 | 12 | s3 = boto3.client( 13 | "s3", 14 | endpoint_url=f"https://s3.{REGION}.amazonaws.com", 15 | config=Config( 16 | s3={"addressing_style": "virtual"}, region_name=REGION, signature_version="s3v4" 17 | ), 18 | ) 19 | logger = Logger() 20 | 21 | 22 | def s3_key_exists(bucket, key): 23 | try: 24 | s3.head_object(Bucket=bucket, Key=key) 25 | return True 26 | except: 27 | return False 28 | 29 | 30 | @logger.inject_lambda_context(log_event=True) 31 | def lambda_handler(event, context): 32 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 33 | file_name_full = event["queryStringParameters"]["file_name"] 34 | file_name = file_name_full.split(".pdf")[0] 35 | 36 | exists = s3_key_exists(BUCKET, f"{user_id}/{file_name_full}/{file_name_full}") 37 | 38 | logger.info( 39 | { 40 | "user_id": user_id, 41 | "file_name_full": file_name_full, 42 | "file_name": file_name, 43 | "exists": exists, 44 | } 45 | ) 46 | 47 | if exists: 48 | suffix = shortuuid.ShortUUID().random(length=4) 49 | key = f"{user_id}/{file_name}-{suffix}.pdf/{file_name}-{suffix}.pdf" 50 | else: 51 | key = f"{user_id}/{file_name}.pdf/{file_name}.pdf" 52 | 53 | presigned_url = s3.generate_presigned_url( 54 | ClientMethod="put_object", 55 | Params={ 56 | "Bucket": BUCKET, 57 | "Key": key, 58 | "ContentType": "application/pdf", 59 | }, 60 | ExpiresIn=300, 61 | HttpMethod="PUT", 62 | ) 63 | 64 | return { 65 | "statusCode": 200, 66 | "headers": { 67 | "Content-Type": "application/json", 68 | "Access-Control-Allow-Headers": "*", 69 | "Access-Control-Allow-Origin": "*", 70 | "Access-Control-Allow-Methods": "*", 71 | }, 72 | "body": json.dumps({"presignedurl": presigned_url}), 73 | } 74 | -------------------------------------------------------------------------------- /backend/src/generate_presigned_url/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.28.57 2 | botocore==1.31.57 3 | shortuuid==1.0.11 -------------------------------------------------------------------------------- /backend/src/generate_response/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/generate_response/__init__.py -------------------------------------------------------------------------------- /backend/src/generate_response/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import boto3 4 | from aws_lambda_powertools import Logger 5 | from langchain.memory import ConversationBufferMemory 6 | from langchain.chains import ConversationalRetrievalChain 7 | from langchain_community.chat_message_histories import DynamoDBChatMessageHistory 8 | from langchain_community.vectorstores import FAISS 9 | from langchain_aws.chat_models import ChatBedrock 10 | from langchain_aws.embeddings import BedrockEmbeddings 11 | 12 | 13 | MEMORY_TABLE = os.environ["MEMORY_TABLE"] 14 | BUCKET = os.environ["BUCKET"] 15 | MODEL_ID = os.environ["MODEL_ID"] 16 | EMBEDDING_MODEL_ID = os.environ["EMBEDDING_MODEL_ID"] 17 | 18 | s3 = boto3.client("s3") 19 | logger = Logger() 20 | 21 | 22 | def get_embeddings(): 23 | bedrock_runtime = boto3.client( 24 | service_name="bedrock-runtime", 25 | region_name="us-east-1", 26 | ) 27 | 28 | embeddings = BedrockEmbeddings( 29 | model_id=EMBEDDING_MODEL_ID, 30 | client=bedrock_runtime, 31 | region_name="us-east-1", 32 | ) 33 | return embeddings 34 | 35 | def get_faiss_index(embeddings, user, file_name): 36 | s3.download_file(BUCKET, f"{user}/{file_name}/index.faiss", "/tmp/index.faiss") 37 | s3.download_file(BUCKET, f"{user}/{file_name}/index.pkl", "/tmp/index.pkl") 38 | faiss_index = FAISS.load_local("/tmp", embeddings, allow_dangerous_deserialization=True) 39 | return faiss_index 40 | 41 | def create_memory(conversation_id): 42 | message_history = DynamoDBChatMessageHistory( 43 | table_name=MEMORY_TABLE, session_id=conversation_id 44 | ) 45 | 46 | memory = ConversationBufferMemory( 47 | memory_key="chat_history", 48 | chat_memory=message_history, 49 | input_key="question", 50 | output_key="answer", 51 | return_messages=True, 52 | ) 53 | return memory 54 | 55 | def bedrock_chain(faiss_index, memory, human_input, bedrock_runtime): 56 | 57 | chat = ChatBedrock( 58 | model_id=MODEL_ID, 59 | model_kwargs={'temperature': 0.0} 60 | ) 61 | 62 | chain = ConversationalRetrievalChain.from_llm( 63 | llm=chat, 64 | chain_type="stuff", 65 | retriever=faiss_index.as_retriever(), 66 | memory=memory, 67 | return_source_documents=True, 68 | ) 69 | 70 | response = chain.invoke({"question": human_input}) 71 | 72 | return response 73 | 74 | @logger.inject_lambda_context(log_event=True) 75 | def lambda_handler(event, context): 76 | event_body = json.loads(event["body"]) 77 | file_name = event_body["fileName"] 78 | human_input = event_body["prompt"] 79 | conversation_id = event["pathParameters"]["conversationid"] 80 | user = event["requestContext"]["authorizer"]["claims"]["sub"] 81 | 82 | embeddings = get_embeddings() 83 | faiss_index = get_faiss_index(embeddings, user, file_name) 84 | memory = create_memory(conversation_id) 85 | bedrock_runtime = boto3.client( 86 | service_name="bedrock-runtime", 87 | region_name="us-east-1", 88 | ) 89 | 90 | response = bedrock_chain(faiss_index, memory, human_input, bedrock_runtime) 91 | if response: 92 | print(f"{MODEL_ID} -\nPrompt: {human_input}\n\nResponse: {response['answer']}") 93 | else: 94 | raise ValueError(f"Unsupported model ID: {MODEL_ID}") 95 | 96 | logger.info(str(response['answer'])) 97 | 98 | return { 99 | "statusCode": 200, 100 | "headers": { 101 | "Content-Type": "application/json", 102 | "Access-Control-Allow-Headers": "*", 103 | "Access-Control-Allow-Origin": "*", 104 | "Access-Control-Allow-Methods": "*", 105 | }, 106 | "body": json.dumps(response['answer']), 107 | } -------------------------------------------------------------------------------- /backend/src/generate_response/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | botocore 3 | faiss-cpu==1.7.4 4 | langchain==0.3.21 5 | langchain-community==0.3.20 6 | langchain-aws==0.2.17 7 | urllib3 -------------------------------------------------------------------------------- /backend/src/get_all_documents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/get_all_documents/__init__.py -------------------------------------------------------------------------------- /backend/src/get_all_documents/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | import boto3 3 | from boto3.dynamodb.conditions import Key 4 | from aws_lambda_powertools import Logger 5 | 6 | 7 | DOCUMENT_TABLE = os.environ["DOCUMENT_TABLE"] 8 | 9 | 10 | ddb = boto3.resource("dynamodb") 11 | document_table = ddb.Table(DOCUMENT_TABLE) 12 | logger = Logger() 13 | 14 | 15 | @logger.inject_lambda_context(log_event=True) 16 | def lambda_handler(event, context): 17 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 18 | 19 | response = document_table.query(KeyConditionExpression=Key("userid").eq(user_id)) 20 | items = sorted(response["Items"], key=lambda item: item["created"], reverse=True) 21 | for item in items: 22 | item["conversations"] = sorted( 23 | item["conversations"], key=lambda conv: conv["created"], reverse=True 24 | ) 25 | logger.info({"items": items}) 26 | 27 | return { 28 | "statusCode": 200, 29 | "headers": { 30 | "Content-Type": "application/json", 31 | "Access-Control-Allow-Headers": "*", 32 | "Access-Control-Allow-Origin": "*", 33 | "Access-Control-Allow-Methods": "*", 34 | }, 35 | "body": json.dumps(items, default=str), 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/get_document/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/get_document/__init__.py -------------------------------------------------------------------------------- /backend/src/get_document/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | import boto3 3 | from boto3.dynamodb.conditions import Key 4 | from aws_lambda_powertools import Logger 5 | 6 | 7 | DOCUMENT_TABLE = os.environ["DOCUMENT_TABLE"] 8 | MEMORY_TABLE = os.environ["MEMORY_TABLE"] 9 | 10 | 11 | ddb = boto3.resource("dynamodb") 12 | document_table = ddb.Table(DOCUMENT_TABLE) 13 | memory_table = ddb.Table(MEMORY_TABLE) 14 | logger = Logger() 15 | 16 | 17 | @logger.inject_lambda_context(log_event=True) 18 | def lambda_handler(event, context): 19 | user_id = event["requestContext"]["authorizer"]["claims"]["sub"] 20 | document_id = event["pathParameters"]["documentid"] 21 | conversation_id = event["pathParameters"]["conversationid"] 22 | 23 | response = document_table.get_item( 24 | Key={"userid": user_id, "documentid": document_id} 25 | ) 26 | document = response["Item"] 27 | document["conversations"] = sorted( 28 | document["conversations"], key=lambda conv: conv["created"], reverse=True 29 | ) 30 | logger.info({"document": document}) 31 | 32 | response = memory_table.get_item(Key={"SessionId": conversation_id}) 33 | messages = response["Item"]["History"] 34 | logger.info({"messages": messages}) 35 | 36 | return { 37 | "statusCode": 200, 38 | "headers": { 39 | "Content-Type": "application/json", 40 | "Access-Control-Allow-Headers": "*", 41 | "Access-Control-Allow-Origin": "*", 42 | "Access-Control-Allow-Methods": "*", 43 | }, 44 | "body": json.dumps( 45 | { 46 | "conversationid": conversation_id, 47 | "document": document, 48 | "messages": messages, 49 | }, 50 | default=str, 51 | ), 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/upload_trigger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/backend/src/upload_trigger/__init__.py -------------------------------------------------------------------------------- /backend/src/upload_trigger/main.py: -------------------------------------------------------------------------------- 1 | import os, json 2 | from datetime import datetime 3 | import boto3 4 | import PyPDF2 5 | import shortuuid 6 | import urllib 7 | from aws_lambda_powertools import Logger 8 | 9 | DOCUMENT_TABLE = os.environ["DOCUMENT_TABLE"] 10 | MEMORY_TABLE = os.environ["MEMORY_TABLE"] 11 | QUEUE = os.environ["QUEUE"] 12 | BUCKET = os.environ["BUCKET"] 13 | 14 | 15 | ddb = boto3.resource("dynamodb") 16 | document_table = ddb.Table(DOCUMENT_TABLE) 17 | memory_table = ddb.Table(MEMORY_TABLE) 18 | sqs = boto3.client("sqs") 19 | s3 = boto3.client("s3") 20 | logger = Logger() 21 | 22 | 23 | @logger.inject_lambda_context(log_event=True) 24 | def lambda_handler(event, context): 25 | key = urllib.parse.unquote_plus(event["Records"][0]["s3"]["object"]["key"]) 26 | split = key.split("/") 27 | user_id = split[0] 28 | file_name = split[1] 29 | 30 | document_id = shortuuid.uuid() 31 | 32 | s3.download_file(BUCKET, key, f"/tmp/{file_name}") 33 | 34 | with open(f"/tmp/{file_name}", "rb") as f: 35 | reader = PyPDF2.PdfReader(f) 36 | pages = str(len(reader.pages)) 37 | 38 | conversation_id = shortuuid.uuid() 39 | 40 | timestamp = datetime.utcnow() 41 | timestamp_str = timestamp.strftime("%Y-%m-%dT%H:%M:%S.%fZ") 42 | 43 | document = { 44 | "userid": user_id, 45 | "documentid": document_id, 46 | "filename": file_name, 47 | "created": timestamp_str, 48 | "pages": pages, 49 | "filesize": str(event["Records"][0]["s3"]["object"]["size"]), 50 | "docstatus": "UPLOADED", 51 | "conversations": [], 52 | } 53 | 54 | conversation = {"conversationid": conversation_id, "created": timestamp_str} 55 | document["conversations"].append(conversation) 56 | 57 | document_table.put_item(Item=document) 58 | 59 | conversation = {"SessionId": conversation_id, "History": []} 60 | memory_table.put_item(Item=conversation) 61 | 62 | message = { 63 | "documentid": document_id, 64 | "key": key, 65 | "user": user_id, 66 | } 67 | sqs.send_message(QueueUrl=QUEUE, MessageBody=json.dumps(message)) 68 | -------------------------------------------------------------------------------- /backend/src/upload_trigger/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.28.57 2 | botocore==1.31.57 3 | PyPDF2==3.0.1 4 | shortuuid==1.0.11 5 | -------------------------------------------------------------------------------- /backend/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | serverless-pdf-chat 5 | 6 | SAM Template for serverless-pdf-chat 7 | 8 | Globals: 9 | Function: 10 | Runtime: python3.11 11 | Handler: main.lambda_handler 12 | Architectures: 13 | - arm64 14 | Tracing: Active 15 | Environment: 16 | Variables: 17 | LOG_LEVEL: INFO 18 | Layers: 19 | - !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python311-arm64:7 20 | 21 | Parameters: 22 | Frontend: 23 | Default: amplify 24 | Type: String 25 | AllowedValues: 26 | - local 27 | - amplify 28 | Repository: 29 | Type: String 30 | ModelId: 31 | Default: "anthropic.claude-3-sonnet-20240229-v1:0" 32 | Type: String 33 | EmbeddingModelId: 34 | Default: "amazon.titan-embed-text-v2:0" 35 | Type: String 36 | 37 | Conditions: 38 | DeployToAmplifyHosting: !Equals 39 | - !Ref Frontend 40 | - amplify 41 | 42 | Resources: 43 | DocumentBucket: 44 | Type: "AWS::S3::Bucket" 45 | Properties: 46 | BucketName: !Sub "${AWS::StackName}-${AWS::Region}-${AWS::AccountId}" 47 | CorsConfiguration: 48 | CorsRules: 49 | - AllowedHeaders: 50 | - "*" 51 | AllowedMethods: 52 | - GET 53 | - PUT 54 | - HEAD 55 | - POST 56 | - DELETE 57 | AllowedOrigins: 58 | - "*" 59 | PublicAccessBlockConfiguration: 60 | BlockPublicAcls: true 61 | BlockPublicPolicy: true 62 | IgnorePublicAcls: true 63 | RestrictPublicBuckets: true 64 | 65 | DocumentBucketPolicy: 66 | Type: "AWS::S3::BucketPolicy" 67 | Properties: 68 | PolicyDocument: 69 | Id: EnforceHttpsPolicy 70 | Version: "2012-10-17" 71 | Statement: 72 | - Sid: EnforceHttpsSid 73 | Effect: Deny 74 | Principal: "*" 75 | Action: "s3:*" 76 | Resource: 77 | - !Sub "arn:aws:s3:::${DocumentBucket}/*" 78 | - !Sub "arn:aws:s3:::${DocumentBucket}" 79 | Condition: 80 | Bool: 81 | "aws:SecureTransport": "false" 82 | Bucket: !Ref DocumentBucket 83 | 84 | EmbeddingQueue: 85 | Type: AWS::SQS::Queue 86 | DeletionPolicy: Delete 87 | UpdateReplacePolicy: Delete 88 | Properties: 89 | VisibilityTimeout: 180 90 | MessageRetentionPeriod: 3600 91 | 92 | EmbeddingQueuePolicy: 93 | Type: AWS::SQS::QueuePolicy 94 | Properties: 95 | Queues: 96 | - !Ref EmbeddingQueue 97 | PolicyDocument: 98 | Version: "2012-10-17" 99 | Id: SecureTransportPolicy 100 | Statement: 101 | - Sid: AllowSecureTransportOnly 102 | Effect: Deny 103 | Principal: "*" 104 | Action: "SQS:*" 105 | Resource: "*" 106 | Condition: 107 | Bool: 108 | aws:SecureTransport: false 109 | 110 | DocumentTable: 111 | Type: AWS::DynamoDB::Table 112 | DeletionPolicy: Delete 113 | UpdateReplacePolicy: Delete 114 | Properties: 115 | KeySchema: 116 | - AttributeName: userid 117 | KeyType: HASH 118 | - AttributeName: documentid 119 | KeyType: RANGE 120 | AttributeDefinitions: 121 | - AttributeName: userid 122 | AttributeType: S 123 | - AttributeName: documentid 124 | AttributeType: S 125 | BillingMode: PAY_PER_REQUEST 126 | 127 | MemoryTable: 128 | Type: AWS::DynamoDB::Table 129 | DeletionPolicy: Delete 130 | UpdateReplacePolicy: Delete 131 | Properties: 132 | KeySchema: 133 | - AttributeName: SessionId 134 | KeyType: HASH 135 | AttributeDefinitions: 136 | - AttributeName: SessionId 137 | AttributeType: S 138 | BillingMode: PAY_PER_REQUEST 139 | 140 | CognitoUserPool: 141 | Type: AWS::Cognito::UserPool 142 | DeletionPolicy: Delete 143 | UpdateReplacePolicy: Delete 144 | Properties: 145 | AutoVerifiedAttributes: 146 | - email 147 | UsernameAttributes: 148 | - email 149 | AdminCreateUserConfig: 150 | AllowAdminCreateUserOnly: true 151 | Policies: 152 | PasswordPolicy: 153 | MinimumLength: 8 154 | RequireLowercase: true 155 | RequireNumbers: true 156 | RequireSymbols: true 157 | RequireUppercase: true 158 | 159 | CognitoUserPoolClient: 160 | Type: AWS::Cognito::UserPoolClient 161 | Properties: 162 | UserPoolId: !Ref CognitoUserPool 163 | ClientName: !Ref CognitoUserPool 164 | GenerateSecret: false 165 | 166 | Api: 167 | Type: AWS::Serverless::Api 168 | Properties: 169 | StageName: dev 170 | Auth: 171 | DefaultAuthorizer: CognitoAuthorizer 172 | AddDefaultAuthorizerToCorsPreflight: false 173 | Authorizers: 174 | CognitoAuthorizer: 175 | UserPoolArn: !GetAtt CognitoUserPool.Arn 176 | Cors: 177 | AllowOrigin: "'*'" 178 | AllowHeaders: "'*'" 179 | AllowMethods: "'*'" 180 | 181 | GeneratePresignedUrlFunction: 182 | Type: AWS::Serverless::Function 183 | Properties: 184 | CodeUri: src/generate_presigned_url/ 185 | Policies: 186 | - S3CrudPolicy: 187 | BucketName: !Ref DocumentBucket 188 | Environment: 189 | Variables: 190 | BUCKET: !Ref DocumentBucket 191 | REGION: !Sub ${AWS::Region} 192 | Events: 193 | Root: 194 | Type: Api 195 | Properties: 196 | RestApiId: !Ref Api 197 | Path: /generate_presigned_url 198 | Method: GET 199 | 200 | UploadTriggerFunction: 201 | Type: AWS::Serverless::Function 202 | Properties: 203 | CodeUri: src/upload_trigger/ 204 | Policies: 205 | - DynamoDBCrudPolicy: 206 | TableName: !Ref DocumentTable 207 | - DynamoDBCrudPolicy: 208 | TableName: !Ref MemoryTable 209 | - S3ReadPolicy: 210 | BucketName: !Sub "${AWS::StackName}-${AWS::Region}-${AWS::AccountId}*" 211 | - SQSSendMessagePolicy: 212 | QueueName: !GetAtt EmbeddingQueue.QueueName 213 | Environment: 214 | Variables: 215 | DOCUMENT_TABLE: !Ref DocumentTable 216 | MEMORY_TABLE: !Ref MemoryTable 217 | QUEUE: !GetAtt EmbeddingQueue.QueueName 218 | BUCKET: !Sub "${AWS::StackName}-${AWS::Region}-${AWS::AccountId}" 219 | Events: 220 | S3Event: 221 | Type: S3 222 | Properties: 223 | Bucket: !Ref DocumentBucket 224 | Events: 225 | - s3:ObjectCreated:* 226 | Filter: 227 | S3Key: 228 | Rules: 229 | - Name: suffix 230 | Value: .pdf 231 | 232 | GetDocumentFunction: 233 | Type: AWS::Serverless::Function 234 | Properties: 235 | CodeUri: src/get_document/ 236 | Policies: 237 | - DynamoDBReadPolicy: 238 | TableName: !Ref DocumentTable 239 | - DynamoDBReadPolicy: 240 | TableName: !Ref MemoryTable 241 | Environment: 242 | Variables: 243 | DOCUMENT_TABLE: !Ref DocumentTable 244 | MEMORY_TABLE: !Ref MemoryTable 245 | Events: 246 | Root: 247 | Type: Api 248 | Properties: 249 | RestApiId: !Ref Api 250 | Path: /doc/{documentid}/{conversationid} 251 | Method: GET 252 | 253 | GetAllDocuments: 254 | Type: AWS::Serverless::Function 255 | Properties: 256 | CodeUri: src/get_all_documents/ 257 | Policies: 258 | - DynamoDBReadPolicy: 259 | TableName: !Ref DocumentTable 260 | Environment: 261 | Variables: 262 | DOCUMENT_TABLE: !Ref DocumentTable 263 | Events: 264 | Root: 265 | Type: Api 266 | Properties: 267 | RestApiId: !Ref Api 268 | Path: /doc 269 | Method: GET 270 | 271 | AddConversationFunction: 272 | Type: AWS::Serverless::Function 273 | Properties: 274 | CodeUri: src/add_conversation/ 275 | Policies: 276 | - DynamoDBCrudPolicy: 277 | TableName: !Ref DocumentTable 278 | - DynamoDBCrudPolicy: 279 | TableName: !Ref MemoryTable 280 | Environment: 281 | Variables: 282 | DOCUMENT_TABLE: !Ref DocumentTable 283 | MEMORY_TABLE: !Ref MemoryTable 284 | Events: 285 | Root: 286 | Type: Api 287 | Properties: 288 | RestApiId: !Ref Api 289 | Path: /doc/{documentid} 290 | Method: POST 291 | 292 | GenerateEmbeddingsFunction: 293 | Type: AWS::Serverless::Function 294 | Properties: 295 | CodeUri: src/generate_embeddings/ 296 | Timeout: 180 297 | MemorySize: 2048 298 | Policies: 299 | - SQSPollerPolicy: 300 | QueueName: !GetAtt EmbeddingQueue.QueueName 301 | - S3CrudPolicy: 302 | BucketName: !Ref DocumentBucket 303 | - DynamoDBCrudPolicy: 304 | TableName: !Ref DocumentTable 305 | - Statement: 306 | - Sid: "BedrockScopedAccess" 307 | Effect: "Allow" 308 | Action: "bedrock:InvokeModel" 309 | Resource: !Sub "arn:aws:bedrock:*::foundation-model/${EmbeddingModelId}" 310 | Environment: 311 | Variables: 312 | DOCUMENT_TABLE: !Ref DocumentTable 313 | BUCKET: !Ref DocumentBucket 314 | EMBEDDING_MODEL_ID: !Ref EmbeddingModelId 315 | Events: 316 | EmbeddingQueueEvent: 317 | Type: SQS 318 | Properties: 319 | Queue: !GetAtt EmbeddingQueue.Arn 320 | BatchSize: 1 321 | 322 | GenerateResponseFunction: 323 | Type: AWS::Serverless::Function 324 | Properties: 325 | CodeUri: src/generate_response/ 326 | Timeout: 30 327 | MemorySize: 2048 328 | Policies: 329 | - DynamoDBCrudPolicy: 330 | TableName: !Ref MemoryTable 331 | - S3CrudPolicy: 332 | BucketName: !Ref DocumentBucket 333 | - Statement: 334 | - Sid: "BedrockScopedAccess" 335 | Effect: "Allow" 336 | Action: "bedrock:InvokeModel" 337 | Resource: 338 | - !Sub "arn:aws:bedrock:*::foundation-model/${ModelId}" 339 | - !Sub "arn:aws:bedrock:*::foundation-model/${EmbeddingModelId}" 340 | Environment: 341 | Variables: 342 | MEMORY_TABLE: !Ref MemoryTable 343 | BUCKET: !Ref DocumentBucket 344 | MODEL_ID: !Ref ModelId 345 | EMBEDDING_MODEL_ID: !Ref EmbeddingModelId 346 | Events: 347 | Root: 348 | Type: Api 349 | Properties: 350 | RestApiId: !Ref Api 351 | Path: /{documentid}/{conversationid} 352 | Method: POST 353 | 354 | DeleteDocumentFunction: 355 | Type: AWS::Serverless::Function 356 | Properties: 357 | CodeUri: src/delete_document/ 358 | Policies: 359 | - DynamoDBCrudPolicy: 360 | TableName: !Ref DocumentTable 361 | - DynamoDBCrudPolicy: 362 | TableName: !Ref MemoryTable 363 | - S3CrudPolicy: 364 | BucketName: !Sub "${AWS::StackName}-${AWS::Region}-${AWS::AccountId}*" 365 | Environment: 366 | Variables: 367 | DOCUMENT_TABLE: !Ref DocumentTable 368 | MEMORY_TABLE: !Ref MemoryTable 369 | BUCKET: !Sub "${AWS::StackName}-${AWS::Region}-${AWS::AccountId}" 370 | Events: 371 | Root: 372 | Type: Api 373 | Properties: 374 | RestApiId: !Ref Api 375 | Path: /doc/{documentid} 376 | Method: DELETE 377 | 378 | AmplifyApp: 379 | Type: AWS::Amplify::App 380 | Condition: DeployToAmplifyHosting 381 | Properties: 382 | Name: !Sub "${AWS::StackName}-${AWS::Region}-${AWS::AccountId}" 383 | Repository: !Ref Repository 384 | BuildSpec: | 385 | version: 1 386 | applications: 387 | - frontend: 388 | phases: 389 | preBuild: 390 | commands: 391 | - npm ci 392 | build: 393 | commands: 394 | - npm run build 395 | artifacts: 396 | baseDirectory: dist 397 | files: 398 | - '**/*' 399 | cache: 400 | paths: 401 | - node_modules/**/* 402 | appRoot: frontend 403 | AccessToken: "{{resolve:secretsmanager:serverless-pdf-chat-github-token}}" 404 | EnvironmentVariables: 405 | - Name: AMPLIFY_MONOREPO_APP_ROOT 406 | Value: frontend 407 | - Name: VITE_REGION 408 | Value: !Ref AWS::Region 409 | - Name: VITE_API_ENDPOINT 410 | Value: !Sub "https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/dev/" 411 | - Name: VITE_USER_POOL_ID 412 | Value: !Ref CognitoUserPool 413 | - Name: VITE_USER_POOL_CLIENT_ID 414 | Value: !Ref CognitoUserPoolClient 415 | 416 | AmplifyBranch: 417 | Type: AWS::Amplify::Branch 418 | Condition: DeployToAmplifyHosting 419 | Properties: 420 | BranchName: main 421 | AppId: !GetAtt AmplifyApp.AppId 422 | EnableAutoBuild: true 423 | Stage: PRODUCTION 424 | 425 | Outputs: 426 | CognitoUserPool: 427 | Value: !Ref CognitoUserPool 428 | CognitoUserPoolClient: 429 | Value: !Ref CognitoUserPoolClient 430 | ApiGatewayBaseUrl: 431 | Value: !Sub "https://${Api}.execute-api.${AWS::Region}.${AWS::URLSuffix}/dev/" 432 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /frontend/.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 | 26 | # Vite 27 | .env.development -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DocChat - Chat with a PDF 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@aws-amplify/ui-react": "^6.1.14", 14 | "@headlessui/react": "^1.7.15", 15 | "@heroicons/react": "^2.0.18", 16 | "aws-amplify": "^6.5.0", 17 | "date-fns": "^2.30.0", 18 | "filesize": "^10.0.7", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react-router-dom": "^6.13.0" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/typography": "^0.5.9", 25 | "@types/react": "^18.0.37", 26 | "@types/react-dom": "^18.0.11", 27 | "@typescript-eslint/eslint-plugin": "^5.59.0", 28 | "@typescript-eslint/parser": "^5.59.0", 29 | "@vitejs/plugin-react": "^4.3.4", 30 | "autoprefixer": "^10.4.14", 31 | "eslint": "^8.38.0", 32 | "eslint-plugin-react-hooks": "^4.6.0", 33 | "eslint-plugin-react-refresh": "^0.3.4", 34 | "postcss": "^8.4.24", 35 | "tailwindcss": "^3.3.2", 36 | "typescript": "^5.0.2", 37 | "vite": "^6.2.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/loading-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/public/loading-grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Amplify } from "aws-amplify"; 2 | import { fetchAuthSession } from "aws-amplify/auth"; 3 | import { withAuthenticator } from "@aws-amplify/ui-react"; 4 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 5 | import "./index.css"; 6 | import Layout from "./routes/layout"; 7 | import Documents from "./routes/documents"; 8 | import Chat from "./routes/chat"; 9 | 10 | Amplify.configure({ 11 | Auth: { 12 | Cognito: { 13 | userPoolId: import.meta.env.VITE_USER_POOL_ID, 14 | userPoolClientId: import.meta.env.VITE_USER_POOL_CLIENT_ID, 15 | }, 16 | }, 17 | API: { 18 | REST: { 19 | "serverless-pdf-chat": { 20 | endpoint: import.meta.env.VITE_API_ENDPOINT, 21 | region: import.meta.env.VITE_API_REGION, 22 | }, 23 | }, 24 | }}, { 25 | API: { 26 | REST: { 27 | headers: async () => { 28 | const tokens = (await fetchAuthSession()).tokens; 29 | const jwt = tokens?.idToken?.toString(); 30 | return { 31 | "authorization": `Bearer ${jwt}` 32 | }; 33 | } 34 | } 35 | } 36 | }); 37 | 38 | const router = createBrowserRouter([ 39 | { 40 | path: "/", 41 | element: , 42 | children: [ 43 | { 44 | index: true, 45 | Component: Documents, 46 | }, 47 | { 48 | path: "/doc/:documentid/:conversationid", 49 | Component: Chat, 50 | }, 51 | ], 52 | }, 53 | ]); 54 | 55 | function App() { 56 | return ; 57 | } 58 | 59 | export default withAuthenticator(App, { hideSignUp: true }); 60 | -------------------------------------------------------------------------------- /frontend/src/common/types.ts: -------------------------------------------------------------------------------- 1 | export interface Document { 2 | documentid: string; 3 | userid: string; 4 | filename: string; 5 | filesize: string; 6 | docstatus: string; 7 | created: string; 8 | pages: string; 9 | conversations: { 10 | conversationid: string; 11 | created: string; 12 | }[]; 13 | } 14 | 15 | export interface Conversation { 16 | conversationid: string; 17 | document: Document; 18 | messages: { 19 | type: string; 20 | data: { 21 | content: string; 22 | example: boolean; 23 | additional_kwargs: {}; 24 | }; 25 | }[]; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/common/utilities.ts: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | export function getDateTime(date: string): string { 4 | return format(new Date(date), "MMMM d, yyyy - H:mm"); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/ChatMessages.tsx: -------------------------------------------------------------------------------- 1 | import { PaperAirplaneIcon } from "@heroicons/react/24/outline"; 2 | import Loading from "../../public/loading-dots.svg"; 3 | import { Conversation } from "../common/types"; 4 | 5 | interface ChatMessagesProps { 6 | conversation: Conversation; 7 | messageStatus: string; 8 | handlePromptChange: (event: React.ChangeEvent) => void; 9 | handleKeyPress: (event: React.KeyboardEvent) => void; 10 | prompt: string; 11 | submitMessage: () => Promise; 12 | } 13 | 14 | const ChatMessages: React.FC = ({ 15 | prompt, 16 | conversation, 17 | messageStatus, 18 | submitMessage, 19 | handlePromptChange, 20 | handleKeyPress, 21 | }) => { 22 | return ( 23 |
24 |
25 |
26 | {conversation.messages.map((message, i) => ( 27 |
36 |
37 |

{message.data.content}

38 |
39 |
40 | ))} 41 | {messageStatus === "loading" && ( 42 |
43 | 44 |
45 | )} 46 |
47 |
48 | 49 |
50 |
51 | 67 | {messageStatus === "idle" && ( 68 | 75 | )} 76 | {messageStatus === "loading" && ( 77 | 100 | )} 101 |
102 |
103 |
104 | ); 105 | }; 106 | 107 | export default ChatMessages; 108 | -------------------------------------------------------------------------------- /frontend/src/components/ChatSidebar.tsx: -------------------------------------------------------------------------------- 1 | import DocumentDetail from "./DocumentDetail"; 2 | import { Conversation } from "../common/types"; 3 | import { getDateTime } from "../common/utilities"; 4 | import { Params } from "react-router-dom"; 5 | import { 6 | ChatBubbleLeftRightIcon, 7 | PlusCircleIcon, 8 | } from "@heroicons/react/24/outline"; 9 | 10 | interface ChatSidebarProps { 11 | conversation: Conversation; 12 | params: Params; 13 | addConversation: () => Promise; 14 | switchConversation: (e: React.MouseEvent) => void; 15 | conversationListStatus: "idle" | "loading"; 16 | } 17 | 18 | const ChatSidebar: React.FC = ({ 19 | conversation, 20 | params, 21 | addConversation, 22 | switchConversation, 23 | conversationListStatus, 24 | }) => { 25 | return ( 26 |
27 |
28 | 29 |
30 |
31 | {conversationListStatus === "idle" && ( 32 | 39 | )} 40 | {conversationListStatus === "loading" && ( 41 | 65 | )} 66 | {conversation && 67 | conversation.document.conversations.map((conversation, i) => ( 68 |
69 | {params.conversationid === conversation.conversationid && ( 70 | 79 | )} 80 | {params.conversationid !== conversation.conversationid && ( 81 | 92 | )} 93 |
94 | ))} 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default ChatSidebar; 101 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentDetail.tsx: -------------------------------------------------------------------------------- 1 | import { Document } from "../common/types"; 2 | import { getDateTime } from "../common/utilities"; 3 | import { filesize } from "filesize"; 4 | import { 5 | DocumentIcon, 6 | CircleStackIcon, 7 | ClockIcon, 8 | CheckCircleIcon, 9 | CloudIcon, 10 | CogIcon, 11 | TrashIcon, 12 | } from "@heroicons/react/24/outline"; 13 | import { del } from "aws-amplify/api"; 14 | import {useNavigate} from "react-router-dom"; 15 | import {useState} from "react"; 16 | 17 | interface DocumentDetailProps { 18 | document: Document; 19 | onDocumentDeleted?: (document?: Document) => void; 20 | } 21 | 22 | const DocumentDetail: React.FC = ({document, onDocumentDeleted}) => { 23 | const navigate = useNavigate(); 24 | const [deleteStatus, setDeleteStatus] = useState("idle"); 25 | 26 | const deleteDocument = async (event: React.MouseEvent) => { 27 | event.preventDefault(); 28 | setDeleteStatus("deleting"); 29 | await del({ 30 | apiName: "serverless-pdf-chat", 31 | path: `doc/${document.documentid}`, 32 | }).response; 33 | setDeleteStatus("idle"); 34 | if (onDocumentDeleted) onDocumentDeleted(document); 35 | else navigate(`/`); 36 | }; 37 | 38 | return ( 39 | <> 40 |

41 | {document.filename} 42 |

43 |
44 |
45 | 46 | {document.pages} pages 47 |
48 |
49 | 50 | {filesize(Number(document.filesize)).toString()} 51 |
52 |
53 | 54 | {getDateTime(document.created)} 55 |
56 | {document.docstatus === "UPLOADED" && ( 57 |
58 | 59 | 60 | Awaiting processing 61 | 62 |
63 | )} 64 | {document.docstatus === "PROCESSING" && ( 65 |
66 | 67 | 68 | Processing document 69 | 70 |
71 | )} 72 | {document.docstatus === "READY" && ( 73 |
74 |
75 | 76 | 77 | Ready to chat 78 | 79 |
80 |
81 | 87 |
88 |
89 | )} 90 |
91 | 92 | ); 93 | }; 94 | 95 | export default DocumentDetail; 96 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentList.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { get } from "aws-amplify/api"; 3 | import { Link } from "react-router-dom"; 4 | import DocumentDetail from "./DocumentDetail"; 5 | import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline"; 6 | import { Document } from "../common/types"; 7 | import Loading from "../../public/loading-grid.svg"; 8 | 9 | const DocumentList: React.FC = () => { 10 | const [documents, setDocuments] = useState([]); 11 | const [listStatus, setListStatus] = useState("idle"); 12 | 13 | const fetchData = async () => { 14 | setListStatus("loading"); 15 | const response = await get({ 16 | apiName: "serverless-pdf-chat", path:"doc" 17 | }).response; 18 | const docs = await response.body.json() as unknown as Document[] 19 | setListStatus("idle"); 20 | setDocuments(docs); 21 | }; 22 | 23 | useEffect(() => { 24 | fetchData(); 25 | }, []); 26 | 27 | return ( 28 |
29 |
30 |

My documents

31 | 42 |
43 |
44 | {documents && 45 | documents.length > 0 && 46 | documents.map((document: Document) => ( 47 | 52 | 53 | 54 | ))} 55 |
56 | {listStatus === "idle" && documents.length === 0 && ( 57 |
58 |

There's nothing here yet...

59 |

Upload your first document to get started!

60 |
61 | )} 62 | {listStatus === "loading" && documents.length === 0 && ( 63 |
64 | 65 |
66 | )} 67 |
68 | ); 69 | }; 70 | 71 | export default DocumentList; 72 | -------------------------------------------------------------------------------- /frontend/src/components/DocumentUploader.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useState, useEffect } from "react"; 2 | import { get } from "aws-amplify/api"; 3 | import { filesize } from "filesize"; 4 | import { 5 | DocumentIcon, 6 | CheckCircleIcon, 7 | CloudArrowUpIcon, 8 | XCircleIcon, 9 | ArrowLeftCircleIcon, 10 | } from "@heroicons/react/24/outline"; 11 | 12 | interface DocumentUploaderProps { 13 | onDocumentUploaded?:() => void 14 | } 15 | const DocumentUploader: React.FC = ({onDocumentUploaded}) => { 16 | const [inputStatus, setInputStatus] = useState("idle"); 17 | const [buttonStatus, setButtonStatus] = useState("ready"); 18 | const [selectedFile, setSelectedFile] = useState(null); 19 | 20 | useEffect(() => { 21 | if (selectedFile) { 22 | if (selectedFile.type === "application/pdf") { 23 | setInputStatus("valid"); 24 | } else { 25 | setSelectedFile(null); 26 | } 27 | } 28 | }, [selectedFile]); 29 | 30 | const handleFileChange = (event: ChangeEvent) => { 31 | const file = event.target.files?.[0]; 32 | setSelectedFile(file || null); 33 | }; 34 | 35 | const uploadFile = async () => { 36 | if(selectedFile) { 37 | setButtonStatus("uploading"); 38 | const response = await get({ 39 | apiName: "serverless-pdf-chat", 40 | path: "generate_presigned_url", 41 | options: { 42 | headers: { "Content-Type": "application/json" }, 43 | queryParams: { 44 | "file_name": selectedFile?.name 45 | } 46 | }, 47 | }).response 48 | const presignedUrl = await response.body.json() as { presignedurl: string } 49 | fetch(presignedUrl?.presignedurl, { 50 | method: "PUT", 51 | body: selectedFile, 52 | headers: { "Content-Type": "application/pdf" }, 53 | }).then(() => { 54 | setButtonStatus("success"); 55 | if (onDocumentUploaded) onDocumentUploaded(); 56 | }); 57 | } 58 | }; 59 | 60 | const resetInput = () => { 61 | setSelectedFile(null); 62 | setInputStatus("idle"); 63 | setButtonStatus("ready"); 64 | }; 65 | 66 | return ( 67 |
68 |

Add document

69 | {inputStatus === "idle" && ( 70 |
71 | 92 |
93 | )} 94 | {inputStatus === "valid" && ( 95 |
96 |
97 | <> 98 |
99 | 100 |
101 |

{selectedFile?.name}

102 |

103 | {filesize(selectedFile ? selectedFile.size : 0).toString()} 104 |

105 |
106 |
107 |
108 | {buttonStatus === "ready" && ( 109 | 117 | )} 118 | {buttonStatus === "uploading" && ( 119 | 128 | )} 129 | {buttonStatus === "success" && ( 130 | 138 | )} 139 | {buttonStatus === "ready" && ( 140 | 148 | )} 149 | {buttonStatus === "uploading" && ( 150 | 175 | )} 176 | {buttonStatus === "success" && ( 177 | 186 | )} 187 |
188 | 189 |
190 |
191 | )} 192 |
193 | ); 194 | }; 195 | 196 | export default DocumentUploader; 197 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { CloudIcon } from "@heroicons/react/24/outline"; 2 | import GitHub from "../../public/github.svg"; 3 | 4 | const Footer: React.FC = () => { 5 | return ( 6 |
7 | 26 |
27 | ); 28 | }; 29 | 30 | export default Footer; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { Menu } from "@headlessui/react"; 3 | import { 4 | ArrowLeftOnRectangleIcon, 5 | ChevronDownIcon, 6 | } from "@heroicons/react/24/outline"; 7 | import { ChatBubbleLeftRightIcon } from "@heroicons/react/24/solid"; 8 | 9 | interface NavigationProps { 10 | userInfo: any; 11 | handleSignOutClick: ( 12 | event: React.MouseEvent 13 | ) => Promise; 14 | } 15 | 16 | const Navigation: React.FC = ({ 17 | userInfo, 18 | handleSignOutClick, 19 | }: NavigationProps) => { 20 | return ( 21 | 55 | ); 56 | }; 57 | 58 | export default Navigation; 59 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "@aws-amplify/ui-react/styles.css"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /frontend/src/routes/chat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, KeyboardEvent } from "react"; 2 | import { useParams, useNavigate } from "react-router-dom"; 3 | import { get, post } from "aws-amplify/api"; 4 | import { Conversation } from "../common/types"; 5 | import ChatSidebar from "../components/ChatSidebar"; 6 | import ChatMessages from "../components/ChatMessages"; 7 | import LoadingGrid from "../../public/loading-grid.svg"; 8 | 9 | const Document: React.FC = () => { 10 | const params = useParams(); 11 | const navigate = useNavigate(); 12 | 13 | const [conversation, setConversation] = useState(null); 14 | const [loading, setLoading] = React.useState("idle"); 15 | const [messageStatus, setMessageStatus] = useState("idle"); 16 | const [conversationListStatus, setConversationListStatus] = useState< 17 | "idle" | "loading" 18 | >("idle"); 19 | const [prompt, setPrompt] = useState(""); 20 | 21 | const fetchData = async (conversationid = params.conversationid) => { 22 | setLoading("loading"); 23 | const response = await get({ 24 | apiName: "serverless-pdf-chat", 25 | path: `doc/${params.documentid}/${conversationid}` 26 | }).response 27 | const conversation = await response.body.json() as unknown as Conversation 28 | setConversation(conversation); 29 | setLoading("idle"); 30 | console.log("Foo") 31 | }; 32 | 33 | useEffect(() => { 34 | fetchData(); 35 | }, []); 36 | 37 | const handlePromptChange = (event: React.ChangeEvent) => { 38 | setPrompt(event.target.value); 39 | }; 40 | 41 | const addConversation = async () => { 42 | setConversationListStatus("loading"); 43 | const response = await post({ 44 | apiName: "serverless-pdf-chat", 45 | path: `doc/${params.documentid}` 46 | }).response; 47 | const newConversation = await response.body.json() as unknown as Conversation; 48 | fetchData(newConversation.conversationid); 49 | navigate(`/doc/${params.documentid}/${newConversation.conversationid}`); 50 | setConversationListStatus("idle"); 51 | }; 52 | 53 | const switchConversation = (e: React.MouseEvent) => { 54 | const targetButton = e.target as HTMLButtonElement; 55 | navigate(`/doc/${params.documentid}/${targetButton.id}`); 56 | fetchData(targetButton.id); 57 | }; 58 | 59 | const handleKeyPress = (event: KeyboardEvent) => { 60 | if (event.key == "Enter") { 61 | submitMessage(); 62 | } 63 | }; 64 | 65 | const submitMessage = async () => { 66 | setMessageStatus("loading"); 67 | 68 | if (conversation !== null) { 69 | const previewMessage = { 70 | type: "text", 71 | data: { 72 | content: prompt, 73 | additional_kwargs: {}, 74 | example: false, 75 | }, 76 | }; 77 | 78 | const updatedConversation = { 79 | ...conversation, 80 | messages: [...conversation.messages, previewMessage], 81 | }; 82 | 83 | setConversation(updatedConversation); 84 | 85 | 86 | await post({ 87 | apiName: "serverless-pdf-chat", 88 | path: `${conversation?.document.documentid}/${conversation?.conversationid}`, 89 | options: { 90 | body: { 91 | fileName: conversation?.document.filename, 92 | prompt: prompt, 93 | } 94 | } 95 | }).response; 96 | setPrompt(""); 97 | fetchData(conversation?.conversationid); 98 | setMessageStatus("idle"); 99 | } 100 | }; 101 | 102 | return ( 103 |
104 | {loading === "loading" && !conversation && ( 105 |
106 | 107 |
108 | )} 109 | {conversation && ( 110 |
111 | 118 | 126 |
127 | )} 128 |
129 | ); 130 | }; 131 | 132 | export default Document; 133 | -------------------------------------------------------------------------------- /frontend/src/routes/documents.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from "react"; 2 | import DocumentUploader from "../components/DocumentUploader"; 3 | import DocumentList from "../components/DocumentList"; 4 | 5 | const Documents: React.FC = () => { 6 | const [documentListKey, setDocumentListKey] = useState(1); 7 | const reloadDocuments = () => { 8 | setTimeout(() =>setDocumentListKey(Math.random()), 1000); 9 | } 10 | 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Documents; 21 | -------------------------------------------------------------------------------- /frontend/src/routes/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from "react-router-dom"; 2 | import { useEffect, useState } from "react"; 3 | import { signOut, fetchUserAttributes } from "aws-amplify/auth"; 4 | import Navigation from "../components/Navigation"; 5 | import Footer from "../components/Footer"; 6 | 7 | const Layout: React.FC = () => { 8 | const [userInfo, setUserInfo] = useState(null); 9 | 10 | useEffect(() => { 11 | (async () => { 12 | const attributes = await fetchUserAttributes(); 13 | setUserInfo({attributes}) 14 | })(); 15 | }, []); 16 | 17 | const handleSignOutClick = async ( 18 | event: React.MouseEvent 19 | ) => { 20 | event.preventDefault(); 21 | await signOut(); 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 31 |
32 | 33 |
34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default Layout; 41 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | container: { 7 | padding: "7rem", 8 | center: true, 9 | }, 10 | }, 11 | plugins: [require("@tailwindcss/typography")], 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /preview-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/preview-1.png -------------------------------------------------------------------------------- /preview-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/serverless-pdf-chat/bcd3ca95b265ab0b3cbe565a5c605732acd3a9b9/preview-2.png --------------------------------------------------------------------------------