├── .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 | 
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 |
34 |
--------------------------------------------------------------------------------
/frontend/public/loading-grid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |
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
--------------------------------------------------------------------------------