├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ └── check.yml ├── .gitignore ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOP.md ├── Dockerfile ├── LICENSE ├── README.md ├── cdk.json ├── cdk_stacks.py ├── docker-compose.yml ├── docs ├── architecture │ ├── ServerlessPromptChainingArchitecture-Small.png │ └── ServerlessPromptChainingArchitecture.png └── screenshots │ ├── aws_service_invocation.png │ ├── blog_post.png │ ├── condition_chain.png │ ├── human_input.png │ ├── map_chain.png │ ├── meal_planner.png │ ├── model_invocation.png │ ├── most_popular_repo.png │ ├── movie_pitch.png │ ├── movie_pitch_one_pager.png │ ├── parallel_chain.png │ ├── prompt_templating.png │ ├── sequential_chain.png │ ├── story_writer.png │ ├── trip_planner.png │ ├── trip_planner_itinerary.png │ └── validation.png ├── functions ├── generic │ ├── merge_map_output │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt │ ├── parse_json_response │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt │ └── seek_user_input │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt ├── meal_planner │ └── meal_choose_winner_agent │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt ├── most_popular_repo_bedrock_agent │ ├── agent │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt │ └── github_agent_actions │ │ ├── __init__.py │ │ ├── index.py │ │ ├── openapi-schema.yaml │ │ └── requirements.txt ├── most_popular_repo_langchain │ ├── __init__.py │ ├── index.py │ └── requirements.txt └── trip_planner │ └── pdf_creator │ ├── __init__.py │ ├── index.py │ └── requirements.txt ├── pipeline ├── pipeline_app.py ├── pipeline_stack.py ├── requirements-dev.txt ├── requirements.txt └── test_pipeline_stack.py ├── requirements-dev.txt ├── requirements.txt ├── run-test-execution.sh ├── stacks ├── __init__.py ├── alarms_stack.py ├── blog_post_stack.py ├── meal_planner_stack.py ├── most_popular_repo_bedrock_agent_stack.py ├── most_popular_repo_langchain_stack.py ├── movie_pitch_stack.py ├── story_writer_stack.py ├── trip_planner_stack.py ├── util.py └── webapp_stack.py ├── techniques_bedrock_flows ├── cdk.json ├── functions │ └── parse_json_response │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt ├── run-test-execution.py ├── stacks │ ├── conditional_chain.py │ ├── map_chain.py │ ├── model_invocation.py │ ├── parallel_chain.py │ ├── prompt_templating.py │ └── sequential_chain.py ├── technique_stacks.py ├── test-inputs │ ├── ConditionalChain.json │ ├── Map.json │ ├── ModelInvocation.json │ ├── ParallelChain.json │ ├── PromptTemplating.json │ └── SequentialChain.json └── test_bedrock_flows_techniques_stacks.py ├── techniques_step_functions ├── cdk.json ├── functions │ └── parse_json_response │ │ ├── __init__.py │ │ ├── index.py │ │ └── requirements.txt ├── run-test-execution.sh ├── stacks │ ├── aws_service_invocation.py │ ├── conditional_chain.py │ ├── human_input_chain.py │ ├── map_chain.py │ ├── model_invocation.py │ ├── parallel_chain.py │ ├── prompt_templating.py │ ├── sequential_chain.py │ └── validation_chain.py ├── technique_stacks.py ├── test-inputs │ ├── AwsServiceInvocation.json │ ├── ConditionalChain.json │ ├── HumanInput.json │ ├── Map.json │ ├── ModelInvocation.json │ ├── ParallelChain.json │ ├── PromptTemplating.json │ ├── SequentialChain.json │ └── Validation.json └── test_step_functions_techniques_stacks.py ├── test-inputs ├── BlogPost.json ├── MealPlanner.json ├── MostPopularRepoBedrockAgents.json ├── MostPopularRepoLangchain.json ├── MoviePitch.json ├── StoryWriter.json └── TripPlanner.json ├── test_cdk_stacks.py └── webapp ├── Home.py ├── pages ├── 1_Blog_Post.py ├── 2_Story_Writer.py ├── 3_Trip_Planner.py ├── 4_Movie_Pitch.py ├── 5_Meal_Planner.py ├── 6_Most_Popular_Repo_(Bedrock_Agents).py ├── 6_Most_Popular_Repo_(Langchain).py └── workflow_images │ ├── blog_post.png │ ├── meal_planner.png │ ├── most_popular_repo.png │ ├── movie_pitch.png │ ├── story_writer.png │ └── trip_planner.png ├── requirements.txt └── stepfn.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Python stuff 2 | __pycache__ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | .Python 7 | env 8 | pip-log.txt 9 | pip-delete-this-directory.txt 10 | .tox 11 | .coverage 12 | .coverage.* 13 | .cache 14 | nosetests.xml 15 | coverage.xml 16 | *.cover 17 | *.log 18 | .git 19 | .mypy_cache 20 | .pytest_cache 21 | .hypothesis 22 | 23 | # CDK stuff 24 | cdk* 25 | requirements.txt 26 | stacks/ 27 | pipeline/ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | - package-ecosystem: pip 10 | directory: "/pipeline" 11 | schedule: 12 | interval: monthly 13 | open-pull-requests-limit: 10 14 | versioning-strategy: increase 15 | - package-ecosystem: pip 16 | directory: "/webapp" 17 | schedule: 18 | interval: monthly 19 | open-pull-requests-limit: 10 20 | versioning-strategy: increase 21 | - package-ecosystem: pip 22 | directory: "/functions/meal_planner/meal_choose_winner_agent" 23 | schedule: 24 | interval: monthly 25 | open-pull-requests-limit: 10 26 | versioning-strategy: increase 27 | - package-ecosystem: pip 28 | directory: "/functions/generic/seek_user_input" 29 | schedule: 30 | interval: monthly 31 | open-pull-requests-limit: 10 32 | versioning-strategy: increase 33 | - package-ecosystem: pip 34 | directory: "/functions/generic/merge_map_output" 35 | schedule: 36 | interval: monthly 37 | open-pull-requests-limit: 10 38 | versioning-strategy: increase 39 | - package-ecosystem: pip 40 | directory: "/functions/generic/parse_json_response" 41 | schedule: 42 | interval: monthly 43 | open-pull-requests-limit: 10 44 | versioning-strategy: increase 45 | - package-ecosystem: pip 46 | directory: "/techniques_step_functions/functions/parse_json_response" 47 | schedule: 48 | interval: monthly 49 | open-pull-requests-limit: 10 50 | versioning-strategy: increase 51 | - package-ecosystem: pip 52 | directory: "/techniques_bedrock_flows/functions/parse_json_response" 53 | schedule: 54 | interval: monthly 55 | open-pull-requests-limit: 10 56 | versioning-strategy: increase 57 | - package-ecosystem: pip 58 | directory: "/functions/trip_planner/pdf_creator" 59 | schedule: 60 | interval: monthly 61 | open-pull-requests-limit: 10 62 | versioning-strategy: increase 63 | - package-ecosystem: pip 64 | directory: "/functions/most_popular_repo_bedrock_agent/github_agent_actions" 65 | schedule: 66 | interval: monthly 67 | open-pull-requests-limit: 10 68 | versioning-strategy: increase 69 | - package-ecosystem: pip 70 | directory: "/functions/most_popular_repo_bedrock_agent/agent" 71 | schedule: 72 | interval: monthly 73 | open-pull-requests-limit: 10 74 | versioning-strategy: increase 75 | - package-ecosystem: pip 76 | directory: "/functions/most_popular_repo_langchain" 77 | schedule: 78 | interval: monthly 79 | open-pull-requests-limit: 10 80 | versioning-strategy: increase 81 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | [pull_request, push] 3 | 4 | name: Check 5 | 6 | jobs: 7 | pipeline: 8 | name: Pipeline Stack 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.x' 18 | - name: Synthesize 19 | run: | 20 | cd pipeline/ 21 | npm install -g aws-cdk 22 | 23 | python3 -m venv .venv 24 | source .venv/bin/activate 25 | pip install -r requirements.txt 26 | pip install -r requirements-dev.txt 27 | 28 | pytest 29 | 30 | stacks: 31 | name: Application Stacks 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version: 20 38 | - uses: actions/setup-python@v4 39 | with: 40 | python-version: '3.x' 41 | - name: Synthesize 42 | run: | 43 | npm install -g aws-cdk 44 | 45 | python3 -m venv .venv 46 | source .venv/bin/activate 47 | pip install -r requirements.txt 48 | pip install -r requirements-dev.txt 49 | 50 | pytest test_cdk_stacks.py 51 | cd techniques_bedrock_flows 52 | pytest 53 | cd ../techniques_step_functions 54 | pytest 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CDK asset staging directory 2 | .cdk.staging 3 | cdk.out 4 | cdk.context.json 5 | *.swp 6 | package-lock.json 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/#use-with-ide 117 | .pdm.toml 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ 168 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | queue_conditions: 4 | - base=main 5 | - author=dependabot[bot] 6 | - label=dependencies 7 | - -title~=(WIP|wip) 8 | - -label~=(blocked|do-not-merge) 9 | - -merged 10 | - -closed 11 | merge_conditions: 12 | - status-success=Pipeline Stack 13 | - status-success=Application Stacks 14 | merge_method: squash 15 | 16 | pull_request_rules: 17 | - name: Automatically merge Dependabot PRs 18 | conditions: 19 | - base=main 20 | - author=dependabot[bot] 21 | - label=dependencies 22 | - -title~=(WIP|wip) 23 | - -label~=(blocked|do-not-merge) 24 | - -merged 25 | - -closed 26 | actions: 27 | review: 28 | type: APPROVE 29 | queue: 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # Development guide 2 | 3 | ## Setup 4 | 5 | Install both nodejs and python on your computer. 6 | 7 | Install CDK: 8 | ``` 9 | npm install -g aws-cdk 10 | ``` 11 | 12 | Set up a virtual env: 13 | ``` 14 | python3 -m venv .venv 15 | 16 | source .venv/bin/activate 17 | 18 | find . -name requirements.txt | xargs -I{} pip install -r {} 19 | find . -name requirements-dev.txt | xargs -I{} pip install -r {} 20 | ``` 21 | After this initial setup, you only need to run `source .venv/bin/activate` to use the virtual env for further development. 22 | 23 | ## Deploy the technique examples (AWS Step Functions) 24 | 25 | Deploy all the stacks: 26 | ``` 27 | cd techniques_step_functions 28 | 29 | cdk deploy --app 'python3 technique_stacks.py' --all 30 | ``` 31 | 32 | Run an example input through each flow example. Edit the files in the `test-inputs` directory to change the test execution inputs. 33 | ``` 34 | ./run-test-execution.sh ModelInvocation 35 | 36 | ./run-test-execution.sh PromptTemplating 37 | 38 | ./run-test-execution.sh SequentialChain 39 | 40 | ./run-test-execution.sh ParallelChain 41 | 42 | ./run-test-execution.sh ConditionalChain 43 | 44 | ./run-test-execution.sh HumanInput 45 | 46 | ./run-test-execution.sh Map 47 | 48 | ./run-test-execution.sh AwsServiceInvocation 49 | 50 | ./run-test-execution.sh Validation 51 | ``` 52 | 53 | ## Deploy the technique examples (Amazon Bedrock Flows) 54 | 55 | Deploy all the stacks: 56 | ``` 57 | cd techniques_bedrock_flows 58 | 59 | cdk deploy --app 'python3 technique_stacks.py' --all 60 | ``` 61 | 62 | Run an example input through each flow example. Edit the files in the `test-inputs` directory to change the test execution inputs. 63 | ``` 64 | python3 run-test-execution.py ModelInvocation 65 | 66 | python3 run-test-execution.py PromptTemplating 67 | 68 | python3 run-test-execution.py SequentialChain 69 | 70 | python3 run-test-execution.py ParallelChain 71 | 72 | python3 run-test-execution.py ConditionalChain 73 | 74 | python3 run-test-execution.py HumanInput 75 | 76 | python3 run-test-execution.py Map 77 | 78 | python3 run-test-execution.py AwsServiceInvocation 79 | 80 | python3 run-test-execution.py Validation 81 | ``` 82 | 83 | ## Deploy the demo application 84 | 85 | Fork this repo to your own GitHub account. 86 | Edit the file `cdk_stacks.py`. Search for `parent_domain` and fill in your own DNS domain, such as `my-domain.com`. 87 | The demo application will be hosted at `https://bedrock-serverless-prompt-chaining.my-domain.com`. 88 | Push this change to your fork repository. 89 | 90 | Create a [new GitHub personal access token](https://github.com/settings/tokens/new). 91 | The token only needs the `public_repo` scope. 92 | Copy the generated token, and create a Secrets Manager secret containing the token: 93 | ``` 94 | aws secretsmanager create-secret \ 95 | --region us-west-2 \ 96 | --name BedrockPromptChainGitHubToken \ 97 | --description "For access to public repos for the Bedrock serverless prompt chain demos" \ 98 | --secret-string "{\"token\": \"\"}" 99 | ``` 100 | 101 | 102 | Set up a Weasyprint Lambda layer in your account. One of the examples in the demo application uses this library to generate PDF files. 103 | ``` 104 | wget https://github.com/kotify/cloud-print-utils/releases/download/weasyprint-63.0/weasyprint-layer-python3.13.zip 105 | 106 | aws lambda publish-layer-version \ 107 | --region us-west-2 \ 108 | --layer-name weasyprint \ 109 | --zip-file fileb://weasyprint-layer-python3.13.zip \ 110 | --compatible-runtimes "python3.13" \ 111 | --license-info "MIT" \ 112 | --description "fonts and libs required by weasyprint" 113 | 114 | aws ssm put-parameter --region us-west-2 \ 115 | --name WeasyprintLambdaLayer \ 116 | --type String \ 117 | --value 118 | ``` 119 | 120 | Deploy all the demo stacks: 121 | ``` 122 | cdk deploy --app 'python3 cdk_stacks.py' --all 123 | ``` 124 | 125 | The demo application will be hosted at `https://bedrock-serverless-prompt-chaining.my-domain.com`, 126 | behind Cognito-based user authentication. 127 | To add users that can log into the demo application, select the `bedrock-serverless-prompt-chaining-demo` user pool on the 128 | [Cognito console](https://us-west-2.console.aws.amazon.com/cognito/v2/idp/user-pools?region=us-west-2) 129 | and click "Create user". 130 | 131 | As part of deploying the demo application, an SNS topic `bedrock-serverless-prompt-chaining-notifications` 132 | will be created and will receive notifications about demo failures. 133 | An email address or a [chat bot](https://docs.aws.amazon.com/chatbot/latest/adminguide/setting-up.html) 134 | can be subscribed to the topic to receive notifications when the demo's alarms fire. 135 | 136 | ### Deploy the demo pipeline 137 | 138 | The demo pipeline will automatically keep your deployed demo application in sync with the latest changes in your fork repository. 139 | 140 | Edit the file `pipeline/pipeline_stack.py`. 141 | Search for `owner` and fill in the GitHub account that owns your fork repository. 142 | Push this change to your fork. 143 | 144 | Deploy the pipeline: 145 | ``` 146 | cdk deploy --app 'python3 pipeline_app.py' 147 | ``` 148 | 149 | Activate the CodeStar Connections connection created by the pipeline stack. 150 | Go to the [CodeStar Connections console](https://console.aws.amazon.com/codesuite/settings/connections?region=us-west-2), 151 | select the `bedrock-prompt-chain-repo` connection, and click "Update pending connection". 152 | Then follow the prompts to connect your GitHub account and repos to AWS. 153 | When finished, the `bedrock-prompt-chain-repo` connection should have the "Available" status. 154 | 155 | Go to the [pipeline's page in the CodePipeline console](https://us-west-2.console.aws.amazon.com/codesuite/codepipeline/pipelines/bedrock-serverless-prompt-chaining-demo/view?region=us-west-2), 156 | and click "Release change" to restart the pipeline. 157 | 158 | As part of deploying the demo application, an SNS topic `bedrock-serverless-prompt-chaining-notifications` 159 | will be created and will receive notifications about pipeline failures. 160 | An email address or a [chat bot](https://docs.aws.amazon.com/chatbot/latest/adminguide/setting-up.html) 161 | can be subscribed to the topic to receive notifications when pipeline executions fail. 162 | 163 | ### Test local changes 164 | 165 | Ensure the CDK code compiles: 166 | ``` 167 | cdk synth 168 | ``` 169 | 170 | Run the webapp locally: 171 | ``` 172 | docker compose up --build 173 | ``` 174 | 175 | Changes to Step Functions state machines and Lambda functions can be tested in the cloud using `cdk watch`, 176 | after the demo application has been fully deployed to an AWS account (following the instructions above): 177 | ``` 178 | cdk watch --app 'python3 cdk_stacks.py' \ 179 | PromptChaining-BlogPostDemo \ 180 | PromptChaining-TripPlannerDemo \ 181 | PromptChaining-StoryWriterDemo \ 182 | PromptChaining-MoviePitchDemo \ 183 | PromptChaining-MealPlannerDemo \ 184 | PromptChaining-MostPopularRepoBedrockAgentsDemo \ 185 | PromptChaining-MostPopularRepoLangchainDemo 186 | ``` 187 | 188 | Then in a separate terminal, run test executions in the cloud after making changes to your code. 189 | Edit the files in the `test-inputs` directory to change the test execution inputs. 190 | ``` 191 | ./run-test-execution.sh BlogPost 192 | 193 | ./run-test-execution.sh TripPlanner 194 | 195 | ./run-test-execution.sh StoryWriter 196 | 197 | ./run-test-execution.sh MoviePitch 198 | 199 | ./run-test-execution.sh MealPlanner 200 | 201 | ./run-test-execution.sh MostPopularRepoBedrockAgents 202 | 203 | ./run-test-execution.sh MostPopularRepoLangchain 204 | ``` 205 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/python:3.9-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY webapp/requirements.txt ./requirements.txt 6 | RUN pip3 install -r requirements.txt 7 | COPY webapp/ . 8 | 9 | EXPOSE 8501 10 | HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health 11 | 12 | ENTRYPOINT ["streamlit", "run", "Home.py", "--server.port=8501", "--server.address=0.0.0.0"] 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 cdk_stacks.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "*.md", 9 | "LICENSE", 10 | "cdk*.json", 11 | "pipeline", 12 | "docs", 13 | "requirements*.txt", 14 | "source.bat", 15 | "**/__init__.py", 16 | "**/__pycache__", 17 | "*test*", 18 | "docker-compose.yml" 19 | ] 20 | }, 21 | "context": { 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/core:checkSecretUsage": true, 24 | "@aws-cdk/core:target-partitions": [ 25 | "aws", 26 | "aws-cn" 27 | ], 28 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 29 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/aws-iam:minimizePolicies": true, 32 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 33 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 34 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 35 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 36 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 37 | "@aws-cdk/core:enablePartitionLiterals": true, 38 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 39 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 40 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 41 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 42 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 43 | "@aws-cdk/aws-route53-patters:useCertificate": true, 44 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 45 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 46 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 47 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 48 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 49 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 50 | "@aws-cdk/aws-redshift:columnId": true, 51 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 52 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 53 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 54 | "@aws-cdk/aws-kms:aliasNameRef": true, 55 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 56 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 57 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /cdk_stacks.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | App, 3 | Environment, 4 | ) 5 | from stacks.webapp_stack import WebappStack 6 | from stacks.blog_post_stack import BlogPostStack 7 | from stacks.trip_planner_stack import TripPlannerStack 8 | from stacks.story_writer_stack import StoryWriterStack 9 | from stacks.movie_pitch_stack import MoviePitchStack 10 | from stacks.meal_planner_stack import MealPlannerStack 11 | from stacks.most_popular_repo_bedrock_agent_stack import ( 12 | MostPopularRepoBedrockAgentStack, 13 | ) 14 | from stacks.most_popular_repo_langchain_stack import ( 15 | MostPopularRepoLangchainStack, 16 | ) 17 | from stacks.alarms_stack import AlarmsStack 18 | import os 19 | 20 | 21 | app = App() 22 | env = Environment(account=os.environ["CDK_DEFAULT_ACCOUNT"], region="us-west-2") 23 | WebappStack( 24 | app, 25 | "PromptChaining-StreamlitWebapp", 26 | env=env, 27 | parent_domain="TODO FILL IN", 28 | ) 29 | BlogPostStack( 30 | app, 31 | "PromptChaining-BlogPostDemo", 32 | env=env, 33 | ) 34 | TripPlannerStack( 35 | app, 36 | "PromptChaining-TripPlannerDemo", 37 | env=env, 38 | ) 39 | StoryWriterStack( 40 | app, 41 | "PromptChaining-StoryWriterDemo", 42 | env=env, 43 | ) 44 | MoviePitchStack( 45 | app, 46 | "PromptChaining-MoviePitchDemo", 47 | env=env, 48 | ) 49 | MealPlannerStack( 50 | app, 51 | "PromptChaining-MealPlannerDemo", 52 | env=env, 53 | ) 54 | MostPopularRepoBedrockAgentStack( 55 | app, 56 | "PromptChaining-MostPopularRepoBedrockAgentsDemo", 57 | env=env, 58 | ) 59 | MostPopularRepoLangchainStack( 60 | app, 61 | "PromptChaining-MostPopularRepoLangchainDemo", 62 | env=env, 63 | ) 64 | AlarmsStack( 65 | app, 66 | "PromptChaining-Alarms", 67 | env=env, 68 | ) 69 | app.synth() 70 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | streamlit: 5 | image: amazon-bedrock-serverless-prompt-chaining-demo 6 | build: 7 | dockerfile: ./Dockerfile 8 | context: . 9 | ports: 10 | - "8501:8501" 11 | environment: 12 | - AWS_REGION=us-west-2 13 | volumes: 14 | - type: bind 15 | source: ~/.aws 16 | target: /root/.aws 17 | -------------------------------------------------------------------------------- /docs/architecture/ServerlessPromptChainingArchitecture-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/architecture/ServerlessPromptChainingArchitecture-Small.png -------------------------------------------------------------------------------- /docs/architecture/ServerlessPromptChainingArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/architecture/ServerlessPromptChainingArchitecture.png -------------------------------------------------------------------------------- /docs/screenshots/aws_service_invocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/aws_service_invocation.png -------------------------------------------------------------------------------- /docs/screenshots/blog_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/blog_post.png -------------------------------------------------------------------------------- /docs/screenshots/condition_chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/condition_chain.png -------------------------------------------------------------------------------- /docs/screenshots/human_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/human_input.png -------------------------------------------------------------------------------- /docs/screenshots/map_chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/map_chain.png -------------------------------------------------------------------------------- /docs/screenshots/meal_planner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/meal_planner.png -------------------------------------------------------------------------------- /docs/screenshots/model_invocation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/model_invocation.png -------------------------------------------------------------------------------- /docs/screenshots/most_popular_repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/most_popular_repo.png -------------------------------------------------------------------------------- /docs/screenshots/movie_pitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/movie_pitch.png -------------------------------------------------------------------------------- /docs/screenshots/movie_pitch_one_pager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/movie_pitch_one_pager.png -------------------------------------------------------------------------------- /docs/screenshots/parallel_chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/parallel_chain.png -------------------------------------------------------------------------------- /docs/screenshots/prompt_templating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/prompt_templating.png -------------------------------------------------------------------------------- /docs/screenshots/sequential_chain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/sequential_chain.png -------------------------------------------------------------------------------- /docs/screenshots/story_writer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/story_writer.png -------------------------------------------------------------------------------- /docs/screenshots/trip_planner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/trip_planner.png -------------------------------------------------------------------------------- /docs/screenshots/trip_planner_itinerary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/trip_planner_itinerary.png -------------------------------------------------------------------------------- /docs/screenshots/validation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/docs/screenshots/validation.png -------------------------------------------------------------------------------- /functions/generic/merge_map_output/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/generic/merge_map_output/__init__.py -------------------------------------------------------------------------------- /functions/generic/merge_map_output/index.py: -------------------------------------------------------------------------------- 1 | def handler(event, context): 2 | # Assume every map result contains 2 unique conversation entries (user prompt and assistant response), 3 | # and every map result has the same conversation history before that 4 | 5 | conversation = event[0]["model_outputs"]["conversation"][:-2] 6 | 7 | for item_result in event: 8 | conversation.extend(item_result["model_outputs"]["conversation"][-2:]) 9 | 10 | return {"conversation": conversation} 11 | -------------------------------------------------------------------------------- /functions/generic/merge_map_output/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/generic/merge_map_output/requirements.txt -------------------------------------------------------------------------------- /functions/generic/parse_json_response/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/generic/parse_json_response/__init__.py -------------------------------------------------------------------------------- /functions/generic/parse_json_response/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | from jsonschema import validate 3 | 4 | 5 | # Parse the JSON response string into an object and validate it against the JSON schema. 6 | # Return the validated object. 7 | def handler(event, context): 8 | response_string = event["response_string"] 9 | response_object = json.loads(response_string) 10 | 11 | json_schema = event["json_schema"] 12 | validate(instance=response_object, schema=json_schema) 13 | 14 | return response_object 15 | -------------------------------------------------------------------------------- /functions/generic/parse_json_response/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema==4.24.0 2 | -------------------------------------------------------------------------------- /functions/generic/seek_user_input/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/generic/seek_user_input/__init__.py -------------------------------------------------------------------------------- /functions/generic/seek_user_input/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | # This function receives the question that the agent workflow needs the user to answer, 5 | # and the Step Functions task token that can be used to continue the agent workflow. 6 | # While the agent workflow is waiting for the user to answer the question, the Step Functions 7 | # execution will be paused. 8 | # 9 | # Typically this function would send the user question and the task token to some sort of 10 | # backend API that handles conversation state. The UI would pull the conversation state 11 | # from the backend API and present the question to the user. When the user answers, the UI 12 | # would send the answer to the backend, and the backend would use the user answer and the 13 | # task token to continue the agent workflow. The backend would also send task heartbeats 14 | # to the Step Functions execution while the user session was active. 15 | # 16 | # For the purposes of this demo, this function is a no-op. The Streamlit server polls the 17 | # Step Functions execution directly to get the question and task token, presents the user 18 | # question in the UI, and then sends the user's answer and the task token to the Step Functions 19 | # execution. 20 | def handler(event, context): 21 | print(json.dumps(event, indent=2)) 22 | 23 | return event 24 | -------------------------------------------------------------------------------- /functions/generic/seek_user_input/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/generic/seek_user_input/requirements.txt -------------------------------------------------------------------------------- /functions/meal_planner/meal_choose_winner_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/meal_planner/meal_choose_winner_agent/__init__.py -------------------------------------------------------------------------------- /functions/meal_planner/meal_choose_winner_agent/index.py: -------------------------------------------------------------------------------- 1 | def handler(event, context): 2 | scores = event["parsed_output"]["scores"] 3 | 4 | # Find the highest score 5 | # Scores is a dictionary of chef key -> score 6 | highest_score = 0 7 | winning_chef = None 8 | for key, value in scores.items(): 9 | score = value["score"] 10 | if score > highest_score: 11 | highest_score = score 12 | winning_chef = key 13 | 14 | if not winning_chef: 15 | raise Exception("No winning meal found") 16 | 17 | winning_meal = event[winning_chef]["model_outputs"]["response"] 18 | 19 | return {"winning_chef": winning_chef, "winning_meal": winning_meal} 20 | -------------------------------------------------------------------------------- /functions/meal_planner/meal_choose_winner_agent/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/meal_planner/meal_choose_winner_agent/requirements.txt -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/most_popular_repo_bedrock_agent/agent/__init__.py -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/agent/index.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import re 4 | import uuid 5 | 6 | from botocore.config import Config 7 | 8 | bedrock_agent_client = boto3.client( 9 | "bedrock-agent-runtime", 10 | config=Config(retries={"max_attempts": 6, "mode": "standard"}), 11 | ) 12 | 13 | agent_id = os.environ.get("BEDROCK_AGENT_ID") 14 | agent_alias_id = os.environ.get("BEDROCK_AGENT_ALIAS_ID") 15 | 16 | 17 | def lookup_trending_repo_agent(event, context): 18 | session_id = str(uuid.uuid4()) 19 | example_github_repo = "orgname/reponame" 20 | example_github_url = f"https://github.com/{example_github_repo}" 21 | repo_url = None 22 | 23 | for i in range(3): 24 | input_text = f"What is the top trending repository on GitHub today? Provide only the URL of the top GitHub repository as your answer. For example, if '{example_github_repo}' was the top trending repository, then '{example_github_url}' would be your answer." 25 | if i > 0: 26 | input_text = f"What is the URL of the top trending repository? For example, if '{example_github_repo}' was the top trending repository, then '{example_github_url}' would be your answer." 27 | 28 | response = bedrock_agent_client.invoke_agent( 29 | agentId=agent_id, 30 | agentAliasId=agent_alias_id, 31 | sessionId=session_id, 32 | endSession=False, 33 | inputText=input_text, 34 | ) 35 | 36 | print(f"Session ID: {session_id}") 37 | print(f"Request ID: {response['ResponseMetadata']['RequestId']}") 38 | 39 | chunks = [] 40 | for event in response["completion"]: 41 | chunks.append(event["chunk"]["bytes"].decode("utf-8")) 42 | completion = " ".join(chunks) 43 | 44 | print(f"Completion: {completion}") 45 | 46 | # Find a URL in the response 47 | repo_match = re.search(r"https:\/\/github\.com(?:\/[^\s\/]+){2}", completion) 48 | if repo_match: 49 | url = repo_match.group(0) 50 | if example_github_url not in url: 51 | repo_url = url 52 | break 53 | print(f"Could not extract URL from response {completion}") 54 | 55 | if repo_url is None: 56 | raise Exception("Could not find URL from Bedrock Agent responses") 57 | 58 | return { 59 | "repo": repo_url, 60 | } 61 | 62 | 63 | def summarize_repo_readme_agent(event, context): 64 | session_id = str(uuid.uuid4()) 65 | 66 | response = bedrock_agent_client.invoke_agent( 67 | agentId=agent_id, 68 | agentAliasId=agent_alias_id, 69 | sessionId=session_id, 70 | endSession=False, 71 | inputText=f"Please give me a brief description of the popular GitHub repository {event['repo']}, in 50 - 100 words.", 72 | ) 73 | 74 | print(f"Session ID: {session_id}") 75 | print(f"Request ID: {response['ResponseMetadata']['RequestId']}") 76 | print(f"Repo: {event['repo']}") 77 | 78 | chunks = [] 79 | for response_event in response["completion"]: 80 | chunks.append(response_event["chunk"]["bytes"].decode("utf-8")) 81 | completion = " ".join(chunks) 82 | 83 | print(f"Completion: {completion}") 84 | 85 | return { 86 | "repo": event["repo"], 87 | "summary": completion.strip(), 88 | } 89 | -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/agent/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.27 -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/github_agent_actions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/most_popular_repo_bedrock_agent/github_agent_actions/__init__.py -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/github_agent_actions/index.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import re 4 | import requests 5 | from bs4 import BeautifulSoup 6 | from github import Auth, Github, UnknownObjectException 7 | import json 8 | 9 | secrets_client = boto3.client("secretsmanager") 10 | github_token_secret_name = os.environ.get("GITHUB_TOKEN_SECRET") 11 | 12 | 13 | # Return the contents of the GitHub trending repositories page 14 | def get_github_trending_page_agent_action(): 15 | get_response = requests.get( 16 | "https://github.com/trending", headers={"User-Agent": "Mozilla/5.0"} 17 | ) 18 | 19 | if get_response.status_code != 200: 20 | print(get_response) 21 | raise Exception("Could not retrieve GitHub Trending page") 22 | 23 | soup = BeautifulSoup(get_response.text, "html.parser") 24 | response = [ 25 | "Here are the contents of the GitHub Trending Repositories page. The repositories on the page are ordered by popularity (the highest trending repository is first).", 26 | ] 27 | response += [ 28 | re.sub( 29 | r"\n\s*\n", # De-dupe newlines 30 | "\n", 31 | e.get_text(), 32 | ) 33 | for e in soup.find_all("article", {"class": "Box-row"}) 34 | ] 35 | return "\n".join(response) 36 | 37 | 38 | # Return the contents of a repository's README file 39 | def get_github_repository_readme_agent_action(input): 40 | github_token_secret_value = secrets_client.get_secret_value( 41 | SecretId=github_token_secret_name 42 | ) 43 | github_token = json.loads(github_token_secret_value["SecretString"])["token"] 44 | github_client = Github(auth=Auth.Token(github_token)) 45 | 46 | repo_name = input.replace("https://github.com/", "") 47 | try: 48 | readme_content = ( 49 | github_client.get_repo(repo_name) 50 | .get_readme() 51 | .decoded_content.decode("utf-8") 52 | ) 53 | if len(readme_content) > 5000: 54 | response = f"Here are the first 5,000 characters of the README for {input}." 55 | response += "\n" + readme_content[:5000] 56 | else: 57 | response = f"Here are the full contents of the README for {input}." 58 | response += "\n" + readme_content 59 | return response 60 | except UnknownObjectException: 61 | return f"Could not find a README for the repository {input}. It may not exist in the repository." 62 | 63 | 64 | def handler(event, context): 65 | print(event) 66 | 67 | response_code = 200 68 | response_body = {} 69 | action = event["actionGroup"] 70 | api_path = event["apiPath"] 71 | 72 | try: 73 | if api_path == "/get_trending_github_repositories": 74 | body = get_github_trending_page_agent_action() 75 | response_body = {"contents": str(body)} 76 | elif api_path == "/get_github_repository_readme": 77 | params = event["parameters"] 78 | if not params: 79 | response_code = 400 80 | response_body = {"error": "Missing parameter: repo"} 81 | else: 82 | repo_params = [p for p in params if p["name"] == "repo"] 83 | if not repo_params: 84 | response_code = 400 85 | response_body = {"error": "Missing parameter: repo"} 86 | else: 87 | repo_url = repo_params[0]["value"] 88 | body = get_github_repository_readme_agent_action(repo_url) 89 | response_body = {"contents": str(body)} 90 | else: 91 | response_code = 400 92 | response_body = { 93 | "error": f"{action}::{api_path} is not a valid API, try another one." 94 | } 95 | except Exception as e: 96 | response_code = 500 97 | response_body = {"error": str(e)} 98 | 99 | action_response = { 100 | "actionGroup": event["actionGroup"], 101 | "apiPath": event["apiPath"], 102 | "httpMethod": event["httpMethod"], 103 | "httpStatusCode": response_code, 104 | "responseBody": {"application/json": {"body": json.dumps(response_body)}}, 105 | } 106 | 107 | api_response = {"messageVersion": "1.0", "response": action_response} 108 | print(api_response) 109 | 110 | return api_response 111 | -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/github_agent_actions/openapi-schema.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: GitHub APIs 4 | description: APIs for accessing information about GitHub repositories. 5 | version: 1.0.0 6 | 7 | paths: 8 | /get_trending_github_repositories: 9 | get: 10 | operationId: GetTrendingGitHubRepositories 11 | summary: Retrieves the GitHub Trending Repositories webpage. 12 | description: This API gives you information about which repositories are currently trending on GitHub. 13 | responses: 14 | '200': 15 | description: A successful response will contain the text response of a GET request to the Trending Repositories page. 16 | content: 17 | application/json: 18 | schema: 19 | type: object 20 | properties: 21 | contents: 22 | type: string 23 | description: The contents of the GitHub Trending Repositories page. 24 | required: 25 | - contents 26 | '500': 27 | description: Failed request 28 | content: 29 | application/json: 30 | schema: 31 | type: object 32 | properties: 33 | error: 34 | type: string 35 | required: 36 | - error 37 | 38 | /get_github_repository_readme: 39 | get: 40 | operationId: GetGitHubRepositoryReadme 41 | summary: Retrieves the content of a GitHub repository's README file. 42 | description: This API gives you information about a specific GitHub repository. 43 | parameters: 44 | - in: query 45 | name: repo 46 | schema: 47 | type: string 48 | description: The URL of the GitHub repository. 49 | required: true 50 | responses: 51 | '200': 52 | description: A successful response will contain the contents of the readme. 53 | content: 54 | application/json: 55 | schema: 56 | type: object 57 | properties: 58 | contents: 59 | type: string 60 | description: The contents of the repository readme. 61 | required: 62 | - contents 63 | '500': 64 | description: Failed request 65 | content: 66 | application/json: 67 | schema: 68 | type: object 69 | properties: 70 | error: 71 | type: string 72 | required: 73 | - error 74 | -------------------------------------------------------------------------------- /functions/most_popular_repo_bedrock_agent/github_agent_actions/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.27 2 | requests==2.32.4 3 | PyGithub==2.6.1 4 | beautifulsoup4 5 | -------------------------------------------------------------------------------- /functions/most_popular_repo_langchain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/most_popular_repo_langchain/__init__.py -------------------------------------------------------------------------------- /functions/most_popular_repo_langchain/index.py: -------------------------------------------------------------------------------- 1 | import re 2 | import boto3 3 | from botocore.config import Config 4 | import os 5 | from bs4 import BeautifulSoup 6 | from github import Auth, Github, UnknownObjectException 7 | import json 8 | 9 | from langchain.agents.initialize import initialize_agent 10 | from langchain.agents.agent_types import AgentType 11 | from langchain_community.chat_models.bedrock import BedrockChat 12 | from langchain_community.utilities.requests import TextRequestsWrapper 13 | from langchain_core.tools import Tool 14 | 15 | bedrock_client_config = Config(retries={"max_attempts": 6, "mode": "standard"}) 16 | secrets_client = boto3.client("secretsmanager") 17 | github_token_secret_name = os.environ.get("GITHUB_TOKEN_SECRET") 18 | 19 | 20 | ### Tools ### 21 | def get_github_trending_page(input): 22 | requests_wrapper = TextRequestsWrapper(headers={"User-Agent": "Mozilla/5.0"}) 23 | html = requests_wrapper.get("https://github.com/trending") 24 | soup = BeautifulSoup(html, "html.parser") 25 | response = [ 26 | "Here is the contents of the GitHub Trending Repositories page, inside XML tags. The repositories on the page are ordered by popularity (the highest trending repository is first).", 27 | "", 28 | ] 29 | response += [ 30 | re.sub( 31 | r"\n\s*\n", # De-dupe newlines 32 | "\n", 33 | e.get_text(), 34 | ) 35 | for e in soup.find_all("article", {"class": "Box-row"}) 36 | ] 37 | response += [""] 38 | return "\n".join(response) 39 | 40 | 41 | def get_github_repo_readme(input): 42 | github_token_secret_value = secrets_client.get_secret_value( 43 | SecretId=github_token_secret_name 44 | ) 45 | github_token = json.loads(github_token_secret_value["SecretString"])["token"] 46 | github_client = Github(auth=Auth.Token(github_token)) 47 | 48 | repo_name = input.replace("https://github.com/", "") 49 | try: 50 | readme_content = ( 51 | github_client.get_repo(repo_name) 52 | .get_readme() 53 | .decoded_content.decode("utf-8") 54 | ) 55 | if len(readme_content) > 5000: 56 | response = f"Here are the first 5,000 characters of the README for {input}, inside XML tags." 57 | response += "\n" 58 | response += "\n" + readme_content[:5000] 59 | else: 60 | response = ( 61 | f"Here is the README for {input}, inside XML tags." 62 | ) 63 | response += "\n" 64 | response += "\n" + readme_content 65 | response += "\n" 66 | return response 67 | except UnknownObjectException: 68 | return f"Could not find a README for the repository {input}. It may not exist in the repository." 69 | 70 | 71 | def get_github_langchain_tools(): 72 | return [ 73 | Tool( 74 | name="get_trending_github_repositories", 75 | func=get_github_trending_page, 76 | description="Retrieves the GitHub Trending Repositories webpage. Use this when you need to get information about which repositories are currently trending on GitHub. Provide an empty string as the input. The output will be the text response of a GET request to the Trending Repositories page.", 77 | ), 78 | Tool( 79 | name="get_github_repository_readme", 80 | func=get_github_repo_readme, 81 | description="Retrieves the content of a GitHub repository's README file. Use this when you need to get information about a specific GitHub repository. Provide the URL of the GitHub repository as the input. The output will be the contents of the readme.", 82 | ), 83 | ] 84 | 85 | 86 | ### Agents ### 87 | def lookup_trending_repo_agent(event, context): 88 | llm = BedrockChat( 89 | model_id="anthropic.claude-3-haiku-20240307-v1:0", 90 | model_kwargs={ 91 | "temperature": 0, 92 | "max_tokens": 256, 93 | }, 94 | config=bedrock_client_config, 95 | ) 96 | example_github_url = "https://github.com/orgname/reponame" 97 | repo_url = None 98 | 99 | previous_answers = [] 100 | for i in range(3): 101 | agent = initialize_agent( 102 | get_github_langchain_tools(), 103 | llm, 104 | agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, 105 | verbose=True, 106 | ) 107 | 108 | question = f"What is the top trending repository on GitHub today? Provide only the URL (for example, {example_github_url})." 109 | parsing_error = f'Check your output and make sure it conforms to my instructions! Ensure that you prefix your final answer with "Final Answer: ". Also ensure that you answer the original question: {question}' 110 | agent.handle_parsing_errors = parsing_error 111 | 112 | if len(previous_answers) > 0: 113 | question += f"\nYour previous answers to this question are below, inside XML tags. I was not able to extract a GitHub repository URL for your responses." 114 | question += f"\n{parsing_error}" 115 | for previous_answer in previous_answers: 116 | question += ( 117 | f"\n\n{previous_answer}\n" 118 | ) 119 | 120 | response = agent.run(question).strip() 121 | 122 | # Find a valid repo URL in the response 123 | repo_match = re.search(r"https:\/\/github\.com(?:\/[^\s\/]+){2}", response) 124 | if repo_match: 125 | url = repo_match.group(0) 126 | if example_github_url not in url: 127 | repo_url = url 128 | break 129 | print(f"Could not extract URL from response {response}") 130 | previous_answers.append(response) 131 | 132 | if repo_url is None: 133 | raise Exception("Could not find URL from Langchain agent responses") 134 | 135 | return { 136 | "repo": repo_url, 137 | } 138 | 139 | 140 | def summarize_repo_readme_agent(event, context): 141 | llm = BedrockChat( 142 | model_id="anthropic.claude-3-haiku-20240307-v1:0", 143 | model_kwargs={ 144 | "temperature": 0, 145 | "max_tokens": 500, 146 | }, 147 | config=bedrock_client_config, 148 | ) 149 | 150 | agent = initialize_agent( 151 | get_github_langchain_tools(), 152 | llm, 153 | agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, 154 | verbose=True, 155 | ) 156 | question = f"Briefly describe the popular open source project {event['repo']} in 100 - 200 words." 157 | agent.handle_parsing_errors = f'Check your output and make sure it conforms to my instructions! Ensure that you prefix your final answer with "Final Answer: ". Also ensure that you answer the original question: {question}' 158 | summary = agent.run(question) 159 | 160 | return { 161 | "repo": event["repo"], 162 | "summary": summary.strip(), 163 | } 164 | -------------------------------------------------------------------------------- /functions/most_popular_repo_langchain/requirements.txt: -------------------------------------------------------------------------------- 1 | langchain==0.3.25 2 | langchain-community==0.3.24 3 | requests==2.32.4 4 | PyGithub==2.6.1 5 | boto3==1.38.27 6 | beautifulsoup4 7 | -------------------------------------------------------------------------------- /functions/trip_planner/pdf_creator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/functions/trip_planner/pdf_creator/__init__.py -------------------------------------------------------------------------------- /functions/trip_planner/pdf_creator/index.py: -------------------------------------------------------------------------------- 1 | from md2pdf.core import md2pdf 2 | import os 3 | import tempfile 4 | import uuid 5 | import boto3 6 | 7 | s3_client = boto3.client("s3") 8 | s3_bucket_name = os.environ.get("PDF_BUCKET") 9 | 10 | 11 | def handler(event, context): 12 | location = event["location"] 13 | itinerary_content = f""" 14 | # Your Weekend Vacation 15 | 16 | Here is your three day itinerary for your visit to {location}, created by generative AI. Enjoy! 17 | 18 | {event["itinerary"]} 19 | """ 20 | 21 | s3_object_key = f"itinerary-{uuid.uuid4()}.pdf" 22 | 23 | with tempfile.NamedTemporaryFile(delete=True) as tmp: 24 | md2pdf( 25 | tmp.name, 26 | md_content=itinerary_content, 27 | ) 28 | 29 | s3_client.upload_file( 30 | tmp.name, 31 | s3_bucket_name, 32 | s3_object_key, 33 | ExtraArgs={"ContentType": "application/pdf"}, 34 | ) 35 | 36 | url = s3_client.generate_presigned_url( 37 | "get_object", 38 | Params={"Bucket": s3_bucket_name, "Key": s3_object_key}, 39 | ExpiresIn=900, # 15 minutes 40 | ) 41 | 42 | return { 43 | "itinerary_url": url, 44 | "location": location, 45 | } 46 | -------------------------------------------------------------------------------- /functions/trip_planner/pdf_creator/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.27 2 | md2pdf 3 | -------------------------------------------------------------------------------- /pipeline/pipeline_app.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | App, 3 | Environment, 4 | ) 5 | import os 6 | from pipeline_stack import PipelineStack 7 | 8 | 9 | app = App() 10 | env = Environment(account=os.environ["CDK_DEFAULT_ACCOUNT"], region="us-west-2") 11 | PipelineStack(app, "PromptChainingPipeline", env=env) 12 | app.synth() 13 | -------------------------------------------------------------------------------- /pipeline/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | -------------------------------------------------------------------------------- /pipeline/requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.199.0 2 | constructs>=10.0.0,<11.0.0 3 | -------------------------------------------------------------------------------- /pipeline/test_pipeline_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | from aws_cdk.assertions import Template 3 | 4 | from pipeline_stack import PipelineStack 5 | 6 | 7 | def test_pipeline_stack_synthesizes_properly(): 8 | app = cdk.App() 9 | 10 | test_stack = PipelineStack(app, "TestPipeline") 11 | 12 | # Ensure the template synthesizes successfully 13 | Template.from_stack(test_stack) 14 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | boto3 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aws-cdk-lib==2.199.0 2 | aws-cdk.aws-lambda-python-alpha==2.199.0a0 3 | constructs>=10.0.0,<11.0.0 4 | -------------------------------------------------------------------------------- /run-test-execution.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # TripPlanner, MoviePitch, etc 6 | DEMO_NAME=$1 7 | 8 | AWS_ACCOUNT_ID=`aws sts get-caller-identity --query Account --output text` 9 | 10 | EXECUTION_NAME=local-test-`uuidgen` 11 | 12 | echo "Starting execution $EXECUTION_NAME for state machine PromptChainDemo-$DEMO_NAME" 13 | aws stepfunctions start-execution \ 14 | --region us-west-2 \ 15 | --name $EXECUTION_NAME \ 16 | --state-machine-arn arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:stateMachine:PromptChainDemo-$DEMO_NAME \ 17 | --input file://test-inputs/$DEMO_NAME.json 18 | 19 | echo -e "\nWatch the execution at:" 20 | echo "https://us-west-2.console.aws.amazon.com/states/home?region=us-west-2#/v2/executions/details/arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:execution:PromptChainDemo-$DEMO_NAME:$EXECUTION_NAME" 21 | 22 | echo -ne "\nWaiting for execution to complete..." 23 | while true; do 24 | STATUS=`aws stepfunctions describe-execution --region us-west-2 --query status --output text --execution-arn arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:execution:PromptChainDemo-$DEMO_NAME:$EXECUTION_NAME` 25 | if [ "$STATUS" = "SUCCEEDED" ] || [ "$STATUS" = "FAILED" ] || [ "$STATUS" = "TIMED_OUT" ] || [ "$STATUS" = "ABORTED" ]; then 26 | echo -e "\n\nExecution completed. Status is $STATUS" 27 | if [ "$STATUS" = "SUCCEEDED" ]; then 28 | echo "Output:" 29 | aws stepfunctions describe-execution \ 30 | --execution-arn arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:execution:PromptChainDemo-$DEMO_NAME:$EXECUTION_NAME \ 31 | --query output \ 32 | --output text | jq 33 | exit 0 34 | fi 35 | exit 1 36 | fi 37 | sleep 2 38 | echo -n '.' 39 | done -------------------------------------------------------------------------------- /stacks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/stacks/__init__.py -------------------------------------------------------------------------------- /stacks/alarms_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_cloudwatch as cloudwatch, 4 | aws_cloudwatch_actions as cw_actions, 5 | aws_iam as iam, 6 | aws_stepfunctions as sfn, 7 | aws_sns as sns, 8 | ) 9 | from constructs import Construct 10 | 11 | 12 | class AlarmsStack(Stack): 13 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 14 | super().__init__(scope, construct_id, **kwargs) 15 | 16 | alarms = [] 17 | 18 | for name_suffix in [ 19 | "BlogPost", 20 | "TripPlanner", 21 | "StoryWriter", 22 | "MoviePitch", 23 | "MealPlanner", 24 | "MostPopularRepoBedrockAgents", 25 | "MostPopularRepoLangchain", 26 | ]: 27 | workflow = sfn.StateMachine.from_state_machine_name( 28 | self, f"{name_suffix}Workflow", f"PromptChainDemo-{name_suffix}" 29 | ) 30 | 31 | alarm = cloudwatch.Alarm( 32 | self, 33 | f"{name_suffix}WorkflowFailures", 34 | alarm_name=f"PromptChainDemo-{name_suffix}-Workflow-Failures", 35 | threshold=1, 36 | evaluation_periods=1, 37 | metric=workflow.metric_failed(statistic=cloudwatch.Stats.SUM), 38 | ) 39 | alarms.append(alarm) 40 | 41 | composite_alarm = cloudwatch.CompositeAlarm( 42 | self, 43 | f"CompositeAlarm", 44 | composite_alarm_name="PromptChainDemo-Composite-Alarm", 45 | alarm_rule=cloudwatch.AlarmRule.any_of(*alarms), 46 | ) 47 | 48 | topic = sns.Topic( 49 | self, 50 | "PromptChainDemo-Notifications", 51 | topic_name="bedrock-serverless-prompt-chaining-notifications", 52 | ) 53 | topic.add_to_resource_policy( 54 | iam.PolicyStatement( 55 | actions=["SNS:Publish"], 56 | principals=[ 57 | iam.ServicePrincipal("codestar-notifications.amazonaws.com") 58 | ], 59 | resources=[ 60 | Stack.of(self).format_arn( 61 | service="sns", 62 | resource="bedrock-serverless-prompt-chaining-notifications", 63 | ) 64 | ], 65 | ) 66 | ) 67 | composite_alarm.add_alarm_action(cw_actions.SnsAction(topic)) 68 | -------------------------------------------------------------------------------- /stacks/blog_post_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | aws_stepfunctions as sfn, 5 | ) 6 | from constructs import Construct 7 | 8 | from .util import get_anthropic_claude_invoke_chain 9 | 10 | 11 | class BlogPostStack(Stack): 12 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 13 | super().__init__(scope, construct_id, **kwargs) 14 | 15 | # Agent #1: write book summary 16 | summary_job = get_anthropic_claude_invoke_chain( 17 | self, 18 | "Write a Summary", 19 | prompt=sfn.JsonPath.format( 20 | "Write a 1-2 sentence summary for the book {}.", 21 | sfn.JsonPath.string_at("$$.Execution.Input.novel"), 22 | ), 23 | include_previous_conversation_in_prompt=False, 24 | ) 25 | 26 | # Agent #2: describe the plot 27 | plot_job = get_anthropic_claude_invoke_chain( 28 | self, 29 | "Describe the Plot", 30 | prompt=sfn.JsonPath.format( 31 | "Write a paragraph describing the plot of the book {}.", 32 | sfn.JsonPath.string_at("$$.Execution.Input.novel"), 33 | ), 34 | ) 35 | 36 | # Agent #3: analyze key themes 37 | themes_job = get_anthropic_claude_invoke_chain( 38 | self, 39 | "Analyze Key Themes", 40 | prompt=sfn.JsonPath.format( 41 | "Write a paragraph analyzing the key themes of the book {}.", 42 | sfn.JsonPath.string_at("$$.Execution.Input.novel"), 43 | ), 44 | ) 45 | 46 | # Agent #4: analyze writing style 47 | writing_style_job = get_anthropic_claude_invoke_chain( 48 | self, 49 | "Analyze Writing Style", 50 | prompt=sfn.JsonPath.format( 51 | "Write a paragraph discussing the writing style and tone of the book {}.", 52 | sfn.JsonPath.string_at("$$.Execution.Input.novel"), 53 | ), 54 | ) 55 | 56 | # Agent #5: write the blog post 57 | blog_post_job = get_anthropic_claude_invoke_chain( 58 | self, 59 | "Write the Blog Post", 60 | prompt=sfn.JsonPath.format( 61 | ( 62 | 'Combine your previous responses into a blog post titled "{} - A Literature Review" for my literature blog. ' 63 | "Start the blog post with an introductory paragraph at the beginning and a conclusion paragraph at the end. " 64 | "The blog post should be five paragraphs in total." 65 | ), 66 | sfn.JsonPath.string_at("$$.Execution.Input.novel"), 67 | ), 68 | max_tokens_to_sample=1000, 69 | pass_conversation=False, 70 | ) 71 | 72 | select_final_answer = sfn.Pass( 73 | self, 74 | "Select Final Answer", 75 | output_path="$.model_outputs.response", 76 | ) 77 | 78 | # Hook the agents together into simple pipeline 79 | chain = ( 80 | summary_job.next(plot_job) 81 | .next(themes_job) 82 | .next(writing_style_job) 83 | .next(blog_post_job) 84 | .next(select_final_answer) 85 | ) 86 | 87 | sfn.StateMachine( 88 | self, 89 | "BlogPostWorkflow", 90 | state_machine_name="PromptChainDemo-BlogPost", 91 | definition_body=sfn.DefinitionBody.from_chainable(chain), 92 | timeout=Duration.minutes(5), 93 | ) 94 | -------------------------------------------------------------------------------- /stacks/most_popular_repo_bedrock_agent_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | aws_bedrock as bedrock, 5 | aws_iam as iam, 6 | aws_lambda as lambda_, 7 | aws_lambda_python_alpha as lambda_python, 8 | aws_s3_assets as assets, 9 | aws_secretsmanager as secrets, 10 | aws_stepfunctions as sfn, 11 | aws_stepfunctions_tasks as tasks, 12 | ) 13 | from constructs import Construct 14 | import os 15 | 16 | dirname = os.path.dirname(__file__) 17 | 18 | 19 | class MostPopularRepoBedrockAgentStack(Stack): 20 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 21 | super().__init__(scope, construct_id, **kwargs) 22 | 23 | ### Bedrock Agent resources ### 24 | bedrock_agent_service_role = iam.Role( 25 | self, 26 | "BedrockAgentServiceRole", 27 | role_name="AmazonBedrockExecutionRoleForAgents_BedrockServerlessPromptChain", 28 | assumed_by=iam.ServicePrincipal("bedrock.amazonaws.com"), 29 | ) 30 | 31 | bedrock_agent_service_role.add_to_policy( 32 | iam.PolicyStatement( 33 | effect=iam.Effect.ALLOW, 34 | actions=[ 35 | "bedrock:InvokeModel", 36 | ], 37 | resources=[ 38 | f"arn:aws:bedrock:{self.region}::foundation-model/anthropic.claude-*", 39 | ], 40 | ) 41 | ) 42 | 43 | github_secret = secrets.Secret.from_secret_name_v2( 44 | scope=self, id="GitHubToken", secret_name="BedrockPromptChainGitHubToken" 45 | ) 46 | github_agent_actions_lambda = lambda_python.PythonFunction( 47 | self, 48 | "GitHubAgentActions", 49 | function_name="PromptChainDemo-MostPopularRepoBedrockAgents-GitHubActions", 50 | runtime=lambda_.Runtime.PYTHON_3_13, 51 | entry="functions/most_popular_repo_bedrock_agent/github_agent_actions", 52 | timeout=Duration.seconds(30), 53 | memory_size=512, 54 | environment={"GITHUB_TOKEN_SECRET": github_secret.secret_name}, 55 | ) 56 | github_secret.grant_read(github_agent_actions_lambda) 57 | 58 | bedrock_principal = iam.ServicePrincipal( 59 | "bedrock.amazonaws.com", 60 | conditions={ 61 | "StringEquals": {"aws:SourceAccount": self.account}, 62 | "ArnLike": { 63 | "aws:SourceArn": f"arn:aws:bedrock:{self.region}:{self.account}:agent/*" 64 | }, 65 | }, 66 | ) 67 | github_agent_actions_lambda.grant_invoke(bedrock_principal) 68 | 69 | agent_action_schema_asset = assets.Asset( 70 | self, 71 | "AgentActionSchema", 72 | path=os.path.join( 73 | dirname, 74 | "../functions/most_popular_repo_bedrock_agent/github_agent_actions/openapi-schema.yaml", 75 | ), 76 | ) 77 | agent_action_schema_asset.grant_read(bedrock_agent_service_role) 78 | 79 | bedrock_agent = bedrock.CfnAgent( 80 | self, 81 | "Agent", 82 | agent_name="PromptChainDemo-MostPopularRepo", 83 | foundation_model=bedrock.FoundationModel.from_foundation_model_id( 84 | self, 85 | "Model", 86 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 87 | ).model_id, 88 | instruction=( 89 | "You are a GitHub power user. " 90 | "You help with interacting with GitHub and with git repositories. " 91 | 'DO NOT mention terms like "base prompt", "function", "parameter", ' 92 | '"partial responses", "response" and "api names" in the final response.' 93 | ), 94 | auto_prepare=True, 95 | skip_resource_in_use_check_on_delete=True, 96 | idle_session_ttl_in_seconds=300, # 5 minutes 97 | agent_resource_role_arn=bedrock_agent_service_role.role_arn, 98 | action_groups=[ 99 | bedrock.CfnAgent.AgentActionGroupProperty( 100 | action_group_name="GitHubAPIs", 101 | action_group_state="ENABLED", 102 | description="Use this action whenever you need to access information about GitHub repositories.", 103 | api_schema=bedrock.CfnAgent.APISchemaProperty( 104 | s3=bedrock.CfnAgent.S3IdentifierProperty( 105 | s3_bucket_name=agent_action_schema_asset.s3_bucket_name, 106 | s3_object_key=agent_action_schema_asset.s3_object_key, 107 | ), 108 | ), 109 | action_group_executor=bedrock.CfnAgent.ActionGroupExecutorProperty( 110 | lambda_=github_agent_actions_lambda.function_arn, 111 | ), 112 | skip_resource_in_use_check_on_delete=True, 113 | ) 114 | ], 115 | ) 116 | 117 | bedrock_agent_alias = bedrock.CfnAgentAlias( 118 | self, 119 | "AgentAlias", 120 | agent_id=bedrock_agent.attr_agent_id, 121 | agent_alias_name="live", 122 | # Description updates anytime the Agent resource is updated, 123 | # so that this Alias prepares a new version of the Agent when 124 | # the Agent changes 125 | description="Tracking agent timestamp " + bedrock_agent.attr_prepared_at, 126 | ) 127 | # Ensure agent is fully stabilized before updating the alias 128 | bedrock_agent_alias.add_depends_on(bedrock_agent) 129 | 130 | bedrock_agent_access_policy = iam.PolicyStatement( 131 | effect=iam.Effect.ALLOW, 132 | actions=[ 133 | "bedrock:InvokeAgent", 134 | ], 135 | resources=[ 136 | bedrock_agent_alias.attr_agent_alias_arn, 137 | ], 138 | ) 139 | 140 | ### Agents and Workflow ### 141 | 142 | # Agent #1: look up the highest trending repo on GitHub 143 | lookup_repo_lambda = lambda_python.PythonFunction( 144 | self, 145 | "LookupRepoAgent", 146 | runtime=lambda_.Runtime.PYTHON_3_13, 147 | entry="functions/most_popular_repo_bedrock_agent/agent", 148 | handler="lookup_trending_repo_agent", 149 | bundling=lambda_python.BundlingOptions( 150 | asset_excludes=[".venv", ".mypy_cache", "__pycache__"], 151 | ), 152 | timeout=Duration.minutes(2), 153 | memory_size=512, 154 | environment={ 155 | "BEDROCK_AGENT_ID": bedrock_agent.attr_agent_id, 156 | "BEDROCK_AGENT_ALIAS_ID": bedrock_agent_alias.attr_agent_alias_id, 157 | }, 158 | ) 159 | lookup_repo_lambda.add_to_role_policy(bedrock_agent_access_policy) 160 | 161 | lookup_repo_job = tasks.LambdaInvoke( 162 | self, 163 | "Lookup Repo", 164 | lambda_function=lookup_repo_lambda, 165 | output_path="$.Payload", 166 | ) 167 | 168 | # Agent #2: summarize the repo 169 | summarize_repo_lambda = lambda_python.PythonFunction( 170 | self, 171 | "SummarizeRepoAgent", 172 | runtime=lambda_.Runtime.PYTHON_3_13, 173 | entry="functions/most_popular_repo_bedrock_agent/agent", 174 | handler="summarize_repo_readme_agent", 175 | bundling=lambda_python.BundlingOptions( 176 | asset_excludes=[".venv", ".mypy_cache", "__pycache__"], 177 | ), 178 | timeout=Duration.minutes(2), 179 | memory_size=512, 180 | environment={ 181 | "BEDROCK_AGENT_ID": bedrock_agent.attr_agent_id, 182 | "BEDROCK_AGENT_ALIAS_ID": bedrock_agent_alias.attr_agent_alias_id, 183 | }, 184 | ) 185 | summarize_repo_lambda.add_to_role_policy(bedrock_agent_access_policy) 186 | 187 | summarize_repo_job = tasks.LambdaInvoke( 188 | self, 189 | "Summarize Repo", 190 | lambda_function=summarize_repo_lambda, 191 | output_path="$.Payload", 192 | ) 193 | 194 | # Hook the agents together into a sequential pipeline 195 | chain = lookup_repo_job.next(summarize_repo_job) 196 | 197 | sfn.StateMachine( 198 | self, 199 | "MostPopularRepoWorkflow", 200 | state_machine_name="PromptChainDemo-MostPopularRepoBedrockAgents", 201 | definition_body=sfn.DefinitionBody.from_chainable(chain), 202 | timeout=Duration.minutes(5), 203 | ) 204 | -------------------------------------------------------------------------------- /stacks/most_popular_repo_langchain_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | aws_lambda as lambda_, 5 | aws_lambda_python_alpha as lambda_python, 6 | aws_secretsmanager as secrets, 7 | aws_stepfunctions as sfn, 8 | aws_stepfunctions_tasks as tasks, 9 | ) 10 | from constructs import Construct 11 | 12 | from .util import get_bedrock_iam_policy_statement, get_lambda_bundling_options 13 | 14 | 15 | class MostPopularRepoLangchainStack(Stack): 16 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 17 | super().__init__(scope, construct_id, **kwargs) 18 | 19 | # Agent #1: look up the highest trending repo on GitHub 20 | github_secret = secrets.Secret.from_secret_name_v2( 21 | scope=self, id="GitHubToken", secret_name="BedrockPromptChainGitHubToken" 22 | ) 23 | lookup_repo_lambda = lambda_python.PythonFunction( 24 | self, 25 | "LookupRepoAgent", 26 | runtime=lambda_.Runtime.PYTHON_3_13, 27 | entry="functions/most_popular_repo_langchain", 28 | handler="lookup_trending_repo_agent", 29 | bundling=get_lambda_bundling_options(), 30 | timeout=Duration.minutes(2), 31 | memory_size=512, 32 | environment={"GITHUB_TOKEN_SECRET": github_secret.secret_name}, 33 | ) 34 | lookup_repo_lambda.add_to_role_policy(get_bedrock_iam_policy_statement()) 35 | github_secret.grant_read(lookup_repo_lambda) 36 | 37 | lookup_repo_job = tasks.LambdaInvoke( 38 | self, 39 | "Lookup Repo", 40 | lambda_function=lookup_repo_lambda, 41 | output_path="$.Payload", 42 | ) 43 | 44 | # Agent #2: summarize the repo 45 | summarize_repo_lambda = lambda_python.PythonFunction( 46 | self, 47 | "SummarizeRepoAgent", 48 | runtime=lambda_.Runtime.PYTHON_3_13, 49 | entry="functions/most_popular_repo_langchain", 50 | handler="summarize_repo_readme_agent", 51 | bundling=get_lambda_bundling_options(), 52 | timeout=Duration.minutes(2), 53 | memory_size=512, 54 | environment={"GITHUB_TOKEN_SECRET": github_secret.secret_name}, 55 | ) 56 | summarize_repo_lambda.add_to_role_policy(get_bedrock_iam_policy_statement()) 57 | github_secret.grant_read(summarize_repo_lambda) 58 | 59 | summarize_repo_job = tasks.LambdaInvoke( 60 | self, 61 | "Summarize Repo", 62 | lambda_function=summarize_repo_lambda, 63 | output_path="$.Payload", 64 | ) 65 | 66 | # Hook the agents together into a sequential pipeline 67 | chain = lookup_repo_job.next(summarize_repo_job) 68 | 69 | sfn.StateMachine( 70 | self, 71 | "MostPopularRepoWorkflow", 72 | state_machine_name="PromptChainDemo-MostPopularRepoLangchain", 73 | definition_body=sfn.DefinitionBody.from_chainable(chain), 74 | timeout=Duration.minutes(5), 75 | ) 76 | -------------------------------------------------------------------------------- /stacks/story_writer_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | aws_lambda as lambda_, 5 | aws_lambda_python_alpha as lambda_python, 6 | aws_stepfunctions as sfn, 7 | aws_stepfunctions_tasks as tasks, 8 | ) 9 | from constructs import Construct 10 | 11 | from .util import ( 12 | get_anthropic_claude_invoke_chain, 13 | get_json_response_parser_step, 14 | ) 15 | 16 | 17 | class StoryWriterStack(Stack): 18 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 19 | super().__init__(scope, construct_id, **kwargs) 20 | 21 | # Agent #1: create characters 22 | characters_job = get_anthropic_claude_invoke_chain( 23 | self, 24 | "Generate Characters", 25 | prompt=sfn.JsonPath.format( 26 | """You are an award-winning fiction writer and you are writing a new story about {}. 27 | Before writing the story, describe five characters that will be in the story. 28 | 29 | Your response should be formatted as a JSON array, with each element in the array containing a "name" key for the character's name and a "description" key with the character's description. 30 | An example of a valid response is below, inside XML tags. 31 | 32 | [ 33 | \{ 34 | "name": "Character 1", 35 | "description": "Description for character 1" 36 | \}, 37 | \{ 38 | "name": "Character 2", 39 | "description": "Description for character 2" 40 | \} 41 | ] 42 | 43 | Do not include any other content other than the JSON object in your response. Do not include any XML tags in your response.""", 44 | sfn.JsonPath.string_at("$$.Execution.Input.story_description"), 45 | ), 46 | max_tokens_to_sample=512, 47 | include_previous_conversation_in_prompt=False, 48 | ) 49 | 50 | parse_characters_step = get_json_response_parser_step( 51 | self, 52 | "Parse Characters", 53 | json_schema={ 54 | "type": "array", 55 | "items": { 56 | "type": "object", 57 | "properties": { 58 | "name": {"type": "string"}, 59 | "description": {"type": "string"}, 60 | }, 61 | "required": ["name", "description"], 62 | "additionalProperties": False, 63 | }, 64 | "minItems": 5, 65 | "maxItems": 5, 66 | "uniqueItems": True, 67 | }, 68 | output_key="characters", 69 | result_path="$.parsed_output", 70 | ) 71 | 72 | # Agent #2: create character story arc 73 | character_story_job = get_anthropic_claude_invoke_chain( 74 | self, 75 | "Generate Character Story Arc", 76 | prompt=sfn.JsonPath.format( 77 | "Now describe what will happen in the story to {}, who you previously described as: {}.", 78 | sfn.JsonPath.string_at("$.character.name"), 79 | sfn.JsonPath.string_at("$.character.description"), 80 | ), 81 | max_tokens_to_sample=1024, 82 | include_previous_conversation_in_prompt=True, 83 | ) 84 | 85 | merge_character_stories_lambda = lambda_python.PythonFunction( 86 | self, 87 | "MergeCharacterStoriesAgent", 88 | runtime=lambda_.Runtime.PYTHON_3_13, 89 | entry="functions/generic/merge_map_output", 90 | memory_size=256, 91 | ) 92 | 93 | merge_character_stories_job = tasks.LambdaInvoke( 94 | self, 95 | "Merge Character Stories", 96 | lambda_function=merge_character_stories_lambda, 97 | result_selector={"model_outputs": sfn.JsonPath.object_at("$.Payload")}, 98 | ) 99 | 100 | # Agent #3: write the story 101 | story_job = get_anthropic_claude_invoke_chain( 102 | self, 103 | "Generate the Full Story", 104 | prompt=sfn.JsonPath.format( 105 | "Now write the short story about {}. Respond only with the story content.", 106 | sfn.JsonPath.string_at("$$.Execution.Input.story_description"), 107 | ), 108 | max_tokens_to_sample=2048, 109 | include_previous_conversation_in_prompt=True, 110 | pass_conversation=False, 111 | ) 112 | 113 | select_story = sfn.Pass( 114 | self, 115 | "Select Story", 116 | parameters={ 117 | "story": sfn.JsonPath.string_at("$.model_outputs.response"), 118 | }, 119 | ) 120 | 121 | # Hook the agents together into a workflow that contains a map 122 | chain = ( 123 | characters_job.next(parse_characters_step) 124 | .next( 125 | sfn.Map( 126 | self, 127 | "Character Story Map", 128 | items_path=sfn.JsonPath.string_at("$.parsed_output.characters"), 129 | parameters={ 130 | "character.$": "$$.Map.Item.Value", 131 | "model_outputs.$": "$.model_outputs", 132 | }, 133 | max_concurrency=3, 134 | ).iterator(character_story_job) 135 | ) 136 | .next(merge_character_stories_job) 137 | .next(story_job) 138 | .next(select_story) 139 | ) 140 | 141 | sfn.StateMachine( 142 | self, 143 | "StoryWriterWorkflow", 144 | state_machine_name="PromptChainDemo-StoryWriter", 145 | definition_body=sfn.DefinitionBody.from_chainable(chain), 146 | timeout=Duration.minutes(5), 147 | ) 148 | -------------------------------------------------------------------------------- /stacks/trip_planner_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | RemovalPolicy, 5 | aws_lambda as lambda_, 6 | aws_lambda_python_alpha as lambda_python, 7 | aws_s3 as s3, 8 | aws_ssm as ssm, 9 | aws_stepfunctions as sfn, 10 | aws_stepfunctions_tasks as tasks, 11 | ) 12 | from constructs import Construct 13 | 14 | from .util import ( 15 | get_lambda_bundling_options, 16 | get_anthropic_claude_invoke_chain, 17 | ) 18 | 19 | 20 | class TripPlannerStack(Stack): 21 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 22 | super().__init__(scope, construct_id, **kwargs) 23 | 24 | # Agent #1: suggest places to stay 25 | hotels_job = get_anthropic_claude_invoke_chain( 26 | self, 27 | "Suggest Hotels", 28 | prompt=sfn.JsonPath.format( 29 | """You are a world-class travel agent and an expert on travel to {}. 30 | I am going on a weekend vacation to {}. 31 | Please give me up to 5 suggestions for hotels for my vacation.""", 32 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 33 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 34 | ), 35 | max_tokens_to_sample=512, 36 | include_previous_conversation_in_prompt=False, 37 | pass_conversation=False, 38 | ) 39 | 40 | # Agent #2: suggest places to eat 41 | restaurants_job = get_anthropic_claude_invoke_chain( 42 | self, 43 | "Suggest Restaurants", 44 | prompt=sfn.JsonPath.format( 45 | """You are a world-class travel agent and an expert on travel to {}. 46 | I am going on a weekend vacation to {}. 47 | Please give me suggestions for restaurants for my vacation, including up to 5 suggestions for breakfast, lunch, and dinner.""", 48 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 49 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 50 | ), 51 | max_tokens_to_sample=512, 52 | include_previous_conversation_in_prompt=False, 53 | pass_conversation=False, 54 | ) 55 | 56 | # Agent #3: suggest places to visit 57 | activities_job = get_anthropic_claude_invoke_chain( 58 | self, 59 | "Suggest Activities", 60 | prompt=sfn.JsonPath.format( 61 | """You are a world-class travel agent and an expert on travel to {}. 62 | I am going on a weekend vacation to {}. 63 | Please give me up to 5 suggestions for activities to do or places to visit during my vacation.""", 64 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 65 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 66 | ), 67 | max_tokens_to_sample=512, 68 | include_previous_conversation_in_prompt=False, 69 | pass_conversation=False, 70 | ) 71 | 72 | # Agent #4: form an itinerary 73 | itinerary_job = get_anthropic_claude_invoke_chain( 74 | self, 75 | "Create an Itinerary", 76 | prompt=sfn.JsonPath.format( 77 | """You are a world-class travel agent and an expert on travel to {}. 78 | I am going on a weekend vacation to {} (arriving Friday, leaving Sunday). 79 | 80 | You previously recommended these hotels, inside XML tags. 81 | 82 | {} 83 | 84 | 85 | You previously recommended these restaurants, inside XML tags. 86 | 87 | {} 88 | 89 | 90 | You previously recommended these activities, inside XML tags. 91 | 92 | {} 93 | 94 | 95 | Please give me a daily itinerary for my three-day vacation, based on your previous recommendations. 96 | The itinerary should include one hotel where I will stay for the duration of the vacation. 97 | Each of the three days in the itinerary should have one activity, one restaurant for breakfast, one restaurant for lunch, and one restaurant for dinner. 98 | Each entry in the itinerary should include a short description of your recommended hotel, activity, or restaurant. 99 | The itinerary should be formatted in Markdown format.""", 100 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 101 | sfn.JsonPath.string_at("$$.Execution.Input.location"), 102 | sfn.JsonPath.string_at("$.hotels"), 103 | sfn.JsonPath.string_at("$.restaurants"), 104 | sfn.JsonPath.string_at("$.activities"), 105 | ), 106 | max_tokens_to_sample=512, 107 | include_previous_conversation_in_prompt=False, 108 | pass_conversation=False, 109 | ) 110 | 111 | # Final step: Create the itinerary PDF 112 | pdf_bucket = s3.Bucket( 113 | self, 114 | "PdfBucket", 115 | removal_policy=RemovalPolicy.DESTROY, 116 | block_public_access=s3.BlockPublicAccess.BLOCK_ALL, 117 | lifecycle_rules=[ 118 | s3.LifecycleRule( 119 | id="clean-up-itinerary-files", 120 | expiration=Duration.days(1), 121 | abort_incomplete_multipart_upload_after=Duration.days(1), 122 | noncurrent_version_expiration=Duration.days(1), 123 | noncurrent_versions_to_retain=5, 124 | ) 125 | ], 126 | ) 127 | 128 | weasyprint_layer = lambda_.LayerVersion.from_layer_version_arn( 129 | self, 130 | "WeasyprintLayer", 131 | layer_version_arn=ssm.StringParameter.value_for_string_parameter( 132 | self, parameter_name="WeasyprintLambdaLayer" 133 | ), 134 | ) 135 | 136 | pdf_lambda = lambda_python.PythonFunction( 137 | self, 138 | "PdfCreator", 139 | runtime=lambda_.Runtime.PYTHON_3_13, 140 | entry="functions/trip_planner/pdf_creator", 141 | bundling=get_lambda_bundling_options(), 142 | environment={ 143 | "PDF_BUCKET": pdf_bucket.bucket_name, 144 | "GDK_PIXBUF_MODULE_FILE": "/opt/lib/loaders.cache", 145 | "FONTCONFIG_PATH": "/opt/fonts", 146 | "XDG_DATA_DIRS": "/opt/lib", 147 | }, 148 | timeout=Duration.seconds(30), 149 | memory_size=1024, 150 | layers=[weasyprint_layer], 151 | ) 152 | 153 | pdf_bucket.grant_put(pdf_lambda) 154 | pdf_bucket.grant_read(pdf_lambda) 155 | 156 | pdf_job = tasks.LambdaInvoke( 157 | self, 158 | "Upload the Itinerary", 159 | lambda_function=pdf_lambda, 160 | output_path="$.Payload", 161 | payload=sfn.TaskInput.from_object( 162 | { 163 | "location": sfn.JsonPath.string_at("$$.Execution.Input.location"), 164 | "itinerary": sfn.JsonPath.string_at("$.model_outputs.response"), 165 | } 166 | ), 167 | ) 168 | 169 | # Hook the agents together into a workflow that contains some parallel steps 170 | chain = ( 171 | ( 172 | sfn.Parallel( 173 | self, 174 | "Suggestions", 175 | result_selector={ 176 | "hotels.$": "$[0].model_outputs.response", 177 | "restaurants.$": "$[1].model_outputs.response", 178 | "activities.$": "$[2].model_outputs.response", 179 | }, 180 | ) 181 | .branch(hotels_job) 182 | .branch(restaurants_job) 183 | .branch(activities_job) 184 | ) 185 | .next(itinerary_job) 186 | .next(pdf_job) 187 | ) 188 | 189 | sfn.StateMachine( 190 | self, 191 | "TripPlannerWorkflow", 192 | state_machine_name="PromptChainDemo-TripPlanner", 193 | definition_body=sfn.DefinitionBody.from_chainable(chain), 194 | timeout=Duration.minutes(5), 195 | ) 196 | -------------------------------------------------------------------------------- /stacks/webapp_stack.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | RemovalPolicy, 5 | aws_certificatemanager as acm, 6 | aws_cognito as cognito, 7 | aws_ec2 as ec2, 8 | aws_ecr_assets as ecr_assets, 9 | aws_ecs as ecs, 10 | aws_ecs_patterns as ecs_patterns, 11 | aws_elasticloadbalancingv2 as elb, 12 | aws_elasticloadbalancingv2_actions as elb_actions, 13 | aws_route53 as route53, 14 | aws_secretsmanager as secretsmanager, 15 | aws_stepfunctions as sfn, 16 | ) 17 | from constructs import Construct 18 | 19 | 20 | class WebappStack(Stack): 21 | def __init__( 22 | self, scope: Construct, construct_id: str, parent_domain: str, **kwargs 23 | ) -> None: 24 | super().__init__(scope, construct_id, **kwargs) 25 | 26 | # Set up load-balanced HTTPS Fargate service 27 | vpc = ec2.Vpc( 28 | self, 29 | "VPC", 30 | max_azs=2, 31 | ) 32 | 33 | domain_name = f"bedrock-serverless-prompt-chaining.{parent_domain}" 34 | hosted_zone = route53.HostedZone.from_lookup( 35 | self, "Zone", domain_name=parent_domain 36 | ) 37 | certificate = acm.Certificate( 38 | self, 39 | "Cert", 40 | domain_name=domain_name, 41 | validation=acm.CertificateValidation.from_dns(hosted_zone=hosted_zone), 42 | ) 43 | 44 | cluster = ecs.Cluster(self, "Cluster", vpc=vpc) 45 | 46 | image = ecs.ContainerImage.from_asset( 47 | ".", platform=ecr_assets.Platform.LINUX_AMD64 48 | ) 49 | 50 | fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService( 51 | self, 52 | "StreamlitService", 53 | cluster=cluster, 54 | task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions( 55 | image=image, container_port=8501 # 8501 is the default Streamlit port 56 | ), 57 | public_load_balancer=True, 58 | domain_name=domain_name, 59 | domain_zone=hosted_zone, 60 | certificate=certificate, 61 | ) 62 | 63 | # Configure Streamlit's health check 64 | fargate_service.target_group.configure_health_check( 65 | enabled=True, path="/_stcore/health", healthy_http_codes="200" 66 | ) 67 | 68 | # Speed up deployments 69 | fargate_service.target_group.set_attribute( 70 | key="deregistration_delay.timeout_seconds", 71 | value="10", 72 | ) 73 | 74 | # Grant access to start and query Step Functions exections 75 | for name_suffix in [ 76 | "BlogPost", 77 | "TripPlanner", 78 | "StoryWriter", 79 | "MoviePitch", 80 | "MealPlanner", 81 | "MostPopularRepoBedrockAgents", 82 | "MostPopularRepoLangchain", 83 | ]: 84 | workflow = sfn.StateMachine.from_state_machine_name( 85 | self, f"{name_suffix}Workflow", f"PromptChainDemo-{name_suffix}" 86 | ) 87 | workflow.grant_read(fargate_service.task_definition.task_role) 88 | workflow.grant_start_execution(fargate_service.task_definition.task_role) 89 | workflow.grant_task_response(fargate_service.task_definition.task_role) 90 | 91 | # Add Cognito for authentication 92 | cognito_domain_prefix = "bedrock-serverless-prompt-chaining-demo" 93 | user_pool = cognito.UserPool( 94 | self, 95 | "StreamlitUserPool", 96 | user_pool_name=cognito_domain_prefix, 97 | removal_policy=RemovalPolicy.DESTROY, 98 | account_recovery=cognito.AccountRecovery.NONE, 99 | auto_verify=cognito.AutoVerifiedAttrs(email=True), 100 | sign_in_aliases=cognito.SignInAliases(email=True), 101 | self_sign_up_enabled=False, 102 | password_policy={ 103 | "min_length": 12, 104 | "require_lowercase": False, 105 | "require_digits": False, 106 | "require_uppercase": False, 107 | "require_symbols": False, 108 | }, 109 | ) 110 | 111 | user_pool_domain = cognito.UserPoolDomain( 112 | self, 113 | "StreamlitUserPoolDomain", 114 | user_pool=user_pool, 115 | cognito_domain=cognito.CognitoDomainOptions( 116 | domain_prefix=cognito_domain_prefix 117 | ), 118 | ) 119 | 120 | user_pool_client = user_pool.add_client( 121 | "StreamlitAlbAppClient", 122 | user_pool_client_name="StreamlitAlbAuthentication", 123 | generate_secret=True, 124 | auth_flows=cognito.AuthFlow(user_password=True), 125 | o_auth=cognito.OAuthSettings( 126 | callback_urls=[ 127 | f"https://{domain_name}/oauth2/idpresponse", 128 | f"https://{domain_name}", 129 | ], 130 | flows=cognito.OAuthFlows(authorization_code_grant=True), 131 | scopes=[cognito.OAuthScope.EMAIL], 132 | logout_urls=[f"https://{domain_name}"], 133 | ), 134 | prevent_user_existence_errors=True, 135 | supported_identity_providers=[ 136 | cognito.UserPoolClientIdentityProvider.COGNITO 137 | ], 138 | ) 139 | 140 | fargate_service.listener.add_action( 141 | "authenticate-rule", 142 | priority=1000, 143 | action=elb_actions.AuthenticateCognitoAction( 144 | next=elb.ListenerAction.forward( 145 | target_groups=[fargate_service.target_group] 146 | ), 147 | user_pool=user_pool, 148 | user_pool_client=user_pool_client, 149 | user_pool_domain=user_pool_domain, 150 | ), 151 | conditions=[elb.ListenerCondition.host_headers([domain_name])], 152 | ) 153 | 154 | # Let the load balancer talk to the OIDC provider 155 | lb_security_group = fargate_service.load_balancer.connections.security_groups[0] 156 | lb_security_group.add_egress_rule( 157 | peer=ec2.Peer.any_ipv4(), 158 | connection=ec2.Port( 159 | protocol=ec2.Protocol.TCP, 160 | string_representation="443", 161 | from_port=443, 162 | to_port=443, 163 | ), 164 | description="Outbound HTTPS traffic to the OIDC provider", 165 | ) 166 | 167 | # Disallow accessing the load balancer URL directly 168 | cfn_listener: elb.CfnListener = fargate_service.listener.node.default_child 169 | cfn_listener.default_actions = [ 170 | { 171 | "type": "fixed-response", 172 | "fixedResponseConfig": { 173 | "statusCode": "403", 174 | "contentType": "text/plain", 175 | "messageBody": "This is not a valid endpoint!", 176 | }, 177 | } 178 | ] 179 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 technique_stacks.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "*.md", 9 | "LICENSE", 10 | "cdk*.json", 11 | "pipeline", 12 | "docs", 13 | "requirements*.txt", 14 | "source.bat", 15 | "**/__init__.py", 16 | "**/__pycache__", 17 | "*test*", 18 | "docker-compose.yml" 19 | ] 20 | }, 21 | "context": { 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/core:checkSecretUsage": true, 24 | "@aws-cdk/core:target-partitions": [ 25 | "aws", 26 | "aws-cn" 27 | ], 28 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 29 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/aws-iam:minimizePolicies": true, 32 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 33 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 34 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 35 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 36 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 37 | "@aws-cdk/core:enablePartitionLiterals": true, 38 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 39 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 40 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 41 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 42 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 43 | "@aws-cdk/aws-route53-patters:useCertificate": true, 44 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 45 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 46 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 47 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 48 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 49 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 50 | "@aws-cdk/aws-redshift:columnId": true, 51 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 52 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 53 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 54 | "@aws-cdk/aws-kms:aliasNameRef": true, 55 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 56 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 57 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/functions/parse_json_response/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/techniques_bedrock_flows/functions/parse_json_response/__init__.py -------------------------------------------------------------------------------- /techniques_bedrock_flows/functions/parse_json_response/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | from jsonschema import validate 3 | import os 4 | 5 | json_schema = json.loads(os.environ["SCHEMA"]) 6 | 7 | 8 | # Parse a JSON escaped string into an object and validate it against the JSON schema. 9 | # Return the validated object. 10 | def handler(event, context): 11 | json_string = event["node"]["inputs"][0]["value"] 12 | response_object = json.loads(json_string) 13 | 14 | validate(instance=response_object, schema=json_schema) 15 | 16 | return response_object 17 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/functions/parse_json_response/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema==4.24.0 2 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/run-test-execution.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import sys 4 | 5 | client_runtime = boto3.client("bedrock-agent-runtime") 6 | client_control_plane = boto3.client("bedrock-agent") 7 | flows_paginator = client_control_plane.get_paginator("list_flows") 8 | flow_aliases_paginator = client_control_plane.get_paginator("list_flow_aliases") 9 | 10 | 11 | def main(): 12 | demo_name = sys.argv[1] 13 | 14 | # Find the flow ID for the demo flow 15 | flow_name = f"Flows-{demo_name}" 16 | flow_id = None 17 | for page in flows_paginator.paginate(): 18 | for flow in page["flowSummaries"]: 19 | if flow["name"] == flow_name: 20 | flow_id = flow["id"] 21 | break 22 | if flow_id: 23 | break 24 | if not flow_id: 25 | raise Exception(f"Could not find flow {flow_name}") 26 | 27 | # Find the flow alias ID for the alias named "live" 28 | flow_alias_id = None 29 | for page in flow_aliases_paginator.paginate(flowIdentifier=flow_id): 30 | for flow_alias in page["flowAliasSummaries"]: 31 | if flow_alias["name"] == "live": 32 | flow_alias_id = flow_alias["id"] 33 | break 34 | if flow_alias_id: 35 | break 36 | if not flow_alias_id: 37 | raise Exception( 38 | f"Could not find flow alias {flow_alias_id} for flow {flow_name}" 39 | ) 40 | 41 | # Load the input data 42 | with open(f"test-inputs/{demo_name}.json", "r") as file: 43 | input_data = json.load(file) 44 | 45 | print(f"Invoking flow {flow_id} ({flow_name}) with alias {flow_alias_id} (live)") 46 | 47 | response = client_runtime.invoke_flow( 48 | flowIdentifier=flow_id, 49 | flowAliasIdentifier=flow_alias_id, 50 | inputs=[ 51 | { 52 | "content": input_data, 53 | "nodeName": "Input", 54 | "nodeOutputName": "document", 55 | } 56 | ], 57 | ) 58 | 59 | result = {} 60 | 61 | for event in response.get("responseStream"): 62 | result.update(event) 63 | 64 | if result["flowCompletionEvent"]["completionReason"] == "SUCCESS": 65 | print("Flow invocation was successful! The output of the flow is as follows:\n") 66 | print(result["flowOutputEvent"]["content"]["document"]) 67 | 68 | else: 69 | raise Exception( 70 | "The flow invocation completed unsuccessfully because of the following reason:", 71 | result["flowCompletionEvent"]["completionReason"], 72 | ) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/stacks/model_invocation.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_iam as iam, 5 | ) 6 | from constructs import Construct 7 | 8 | 9 | class FlowsModelInvocation(Stack): 10 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 11 | super().__init__(scope, construct_id, **kwargs) 12 | 13 | model = bedrock.FoundationModel.from_foundation_model_id( 14 | self, 15 | "Model", 16 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 17 | ) 18 | 19 | # Define the prompts 20 | get_summary_prompt = bedrock.CfnPrompt( 21 | self, 22 | "GetSummaryPrompt", 23 | name="Flows-ModelInvocation-GetSummary", 24 | default_variant="default", 25 | variants=[ 26 | bedrock.CfnPrompt.PromptVariantProperty( 27 | name="default", 28 | template_type="TEXT", 29 | # Configure the prompt 30 | template_configuration=bedrock.CfnPrompt.PromptTemplateConfigurationProperty( 31 | text=bedrock.CfnPrompt.TextPromptTemplateConfigurationProperty( 32 | text="Write a 1-2 sentence summary for the book Pride & Prejudice.", 33 | ) 34 | ), 35 | # Configure the model and inference settings 36 | model_id=model.model_id, 37 | inference_configuration=bedrock.CfnPrompt.PromptInferenceConfigurationProperty( 38 | text=bedrock.CfnPrompt.PromptModelInferenceConfigurationProperty( 39 | max_tokens=250, 40 | temperature=1, 41 | ) 42 | ), 43 | ) 44 | ], 45 | ) 46 | 47 | get_summary_prompt_version = bedrock.CfnPromptVersion( 48 | self, 49 | "GetSummaryPromptVersion", 50 | prompt_arn=get_summary_prompt.attr_arn, 51 | # Description updates anytime the Prompt resource is updated, 52 | # so a new version is created when the Prompt changes 53 | description=f"Tracking prompt timestamp {get_summary_prompt.attr_updated_at}", 54 | ) 55 | # Ensure prompt is fully stabilized before creating a new version 56 | get_summary_prompt_version.add_dependency(get_summary_prompt) 57 | 58 | # Configure the flow's nodes and connections between nodes 59 | input_node = bedrock.CfnFlow.FlowNodeProperty( 60 | name="Input", 61 | type="Input", 62 | outputs=[ 63 | bedrock.CfnFlow.FlowNodeOutputProperty( 64 | name="document", 65 | type="String", 66 | ) 67 | ], 68 | ) 69 | 70 | get_summary_node = bedrock.CfnFlow.FlowNodeProperty( 71 | name="Generate_Book_Summary", 72 | type="Prompt", 73 | configuration=bedrock.CfnFlow.FlowNodeConfigurationProperty( 74 | prompt=bedrock.CfnFlow.PromptFlowNodeConfigurationProperty( 75 | source_configuration=bedrock.CfnFlow.PromptFlowNodeSourceConfigurationProperty( 76 | resource=bedrock.CfnFlow.PromptFlowNodeResourceConfigurationProperty( 77 | prompt_arn=get_summary_prompt_version.attr_arn, 78 | ) 79 | ) 80 | ) 81 | ), 82 | inputs=[ 83 | # This input will be ignored, because the prompt is not templated 84 | bedrock.CfnFlow.FlowNodeInputProperty( 85 | name="input", 86 | type="String", 87 | expression="$.data", 88 | ) 89 | ], 90 | outputs=[ 91 | bedrock.CfnFlow.FlowNodeOutputProperty( 92 | name="modelCompletion", 93 | type="String", 94 | ) 95 | ], 96 | ) 97 | 98 | output_node = bedrock.CfnFlow.FlowNodeProperty( 99 | name="Output", 100 | type="Output", 101 | inputs=[ 102 | bedrock.CfnFlow.FlowNodeInputProperty( 103 | name="document", 104 | type="String", 105 | expression="$.data", 106 | ), 107 | ], 108 | ) 109 | 110 | connections = [ 111 | bedrock.CfnFlow.FlowConnectionProperty( 112 | name="_".join([input_node.name, get_summary_node.name]), 113 | type="Data", 114 | source=input_node.name, 115 | target=get_summary_node.name, 116 | configuration=bedrock.CfnFlow.FlowConnectionConfigurationProperty( 117 | data=bedrock.CfnFlow.FlowDataConnectionConfigurationProperty( 118 | source_output=input_node.outputs[0].name, 119 | target_input=get_summary_node.inputs[0].name, 120 | ), 121 | ), 122 | ), 123 | bedrock.CfnFlow.FlowConnectionProperty( 124 | name="_".join([get_summary_node.name, output_node.name]), 125 | type="Data", 126 | source=get_summary_node.name, 127 | target=output_node.name, 128 | configuration=bedrock.CfnFlow.FlowConnectionConfigurationProperty( 129 | data=bedrock.CfnFlow.FlowDataConnectionConfigurationProperty( 130 | source_output=get_summary_node.outputs[0].name, 131 | target_input=output_node.inputs[0].name, 132 | ), 133 | ), 134 | ), 135 | ] 136 | 137 | # Create a role for executing the flow 138 | # See https://docs.aws.amazon.com/bedrock/latest/userguide/flows-permissions.html 139 | bedrock_principal = iam.ServicePrincipal( 140 | "bedrock.amazonaws.com", 141 | conditions={ 142 | "StringEquals": {"aws:SourceAccount": self.account}, 143 | "ArnLike": { 144 | "aws:SourceArn": f"arn:aws:bedrock:{self.region}:{self.account}:flow/*" 145 | }, 146 | }, 147 | ) 148 | 149 | flow_execution_role = iam.Role( 150 | self, 151 | "BedrockFlowsServiceRole", 152 | assumed_by=bedrock_principal, 153 | ) 154 | 155 | flow_execution_role.add_to_policy( 156 | iam.PolicyStatement( 157 | effect=iam.Effect.ALLOW, 158 | actions=["bedrock:InvokeModel"], 159 | resources=[model.model_arn], 160 | ) 161 | ) 162 | flow_execution_role.add_to_policy( 163 | iam.PolicyStatement( 164 | effect=iam.Effect.ALLOW, 165 | actions=["bedrock:RenderPrompt"], 166 | resources=[f"{get_summary_prompt.attr_arn}:*"], 167 | ) 168 | ) 169 | 170 | # Create the flow 171 | flow = bedrock.CfnFlow( 172 | self, 173 | "Flow", 174 | name="Flows-ModelInvocation", 175 | execution_role_arn=flow_execution_role.role_arn, 176 | definition=bedrock.CfnFlow.FlowDefinitionProperty( 177 | nodes=[input_node, get_summary_node, output_node], 178 | connections=connections, 179 | ), 180 | ) 181 | 182 | flow_version = bedrock.CfnFlowVersion( 183 | self, 184 | "FlowVersion", 185 | flow_arn=flow.attr_arn, 186 | # Description updates anytime the Flow resource is updated, 187 | # so a new version is created when the Flow changes 188 | description="Tracking flow timestamp " + flow.attr_updated_at, 189 | ) 190 | # Ensure flow is fully stabilized before creating a new version 191 | flow_version.add_dependency(flow) 192 | 193 | flow_alias = bedrock.CfnFlowAlias( 194 | self, 195 | "FlowAlias", 196 | name="live", 197 | flow_arn=flow.attr_arn, 198 | routing_configuration=[ 199 | bedrock.CfnFlowAlias.FlowAliasRoutingConfigurationListItemProperty( 200 | flow_version=flow_version.attr_version, 201 | ), 202 | ], 203 | ) 204 | # Ensure flow version is fully stabilized before updating the alias 205 | flow_alias.add_dependency(flow_version) 206 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/technique_stacks.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | App, 3 | Environment, 4 | ) 5 | from stacks.model_invocation import FlowsModelInvocation 6 | from stacks.prompt_templating import FlowsPromptTemplating 7 | from stacks.sequential_chain import FlowsSequentialChain 8 | from stacks.parallel_chain import FlowsParallelChain 9 | from stacks.conditional_chain import FlowsConditionalChain 10 | from stacks.map_chain import FlowsMapChain 11 | import os 12 | 13 | 14 | app = App() 15 | env = Environment(account=os.environ["CDK_DEFAULT_ACCOUNT"], region="us-west-2") 16 | FlowsModelInvocation( 17 | app, 18 | "Techniques-Flows-ModelInvocation", 19 | env=env, 20 | ) 21 | FlowsPromptTemplating( 22 | app, 23 | "Techniques-Flows-PromptTemplating", 24 | env=env, 25 | ) 26 | FlowsSequentialChain( 27 | app, 28 | "Techniques-Flows-SequentialChain", 29 | env=env, 30 | ) 31 | FlowsParallelChain( 32 | app, 33 | "Techniques-Flows-ParallelChain", 34 | env=env, 35 | ) 36 | FlowsConditionalChain( 37 | app, 38 | "Techniques-Flows-ConditionalChain", 39 | env=env, 40 | ) 41 | FlowsMapChain( 42 | app, 43 | "Techniques-Flows-Map", 44 | env=env, 45 | ) 46 | app.synth() 47 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/test-inputs/ConditionalChain.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": "Pride and Prejudice" 3 | } 4 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/test-inputs/Map.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": "" 3 | } -------------------------------------------------------------------------------- /techniques_bedrock_flows/test-inputs/ModelInvocation.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": "" 3 | } -------------------------------------------------------------------------------- /techniques_bedrock_flows/test-inputs/ParallelChain.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": "" 3 | } -------------------------------------------------------------------------------- /techniques_bedrock_flows/test-inputs/PromptTemplating.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": "Pride and Prejudice" 3 | } 4 | -------------------------------------------------------------------------------- /techniques_bedrock_flows/test-inputs/SequentialChain.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": "" 3 | } -------------------------------------------------------------------------------- /techniques_bedrock_flows/test_bedrock_flows_techniques_stacks.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | from aws_cdk.assertions import Template 3 | 4 | from stacks.model_invocation import FlowsModelInvocation 5 | from stacks.prompt_templating import FlowsPromptTemplating 6 | from stacks.sequential_chain import FlowsSequentialChain 7 | from stacks.parallel_chain import FlowsParallelChain 8 | from stacks.conditional_chain import FlowsConditionalChain 9 | from stacks.map_chain import FlowsMapChain 10 | 11 | 12 | def test_techniques_bedrock_flows_model_invocation_stack_synthesizes_properly(): 13 | app = cdk.App() 14 | 15 | test_stack = FlowsModelInvocation( 16 | app, 17 | "TestStack", 18 | ) 19 | 20 | # Ensure the template synthesizes successfully 21 | Template.from_stack(test_stack) 22 | 23 | 24 | def test_techniques_bedrock_flows_prompt_templating_stack_synthesizes_properly(): 25 | app = cdk.App() 26 | 27 | test_stack = FlowsPromptTemplating( 28 | app, 29 | "TestStack", 30 | ) 31 | 32 | # Ensure the template synthesizes successfully 33 | Template.from_stack(test_stack) 34 | 35 | 36 | def test_techniques_bedrock_flows_sequential_chain_stack_synthesizes_properly(): 37 | app = cdk.App() 38 | 39 | test_stack = FlowsSequentialChain( 40 | app, 41 | "TestStack", 42 | ) 43 | 44 | # Ensure the template synthesizes successfully 45 | Template.from_stack(test_stack) 46 | 47 | 48 | def test_techniques_bedrock_flows_parallel_chain_stack_synthesizes_properly(): 49 | app = cdk.App() 50 | 51 | test_stack = FlowsParallelChain( 52 | app, 53 | "TestStack", 54 | ) 55 | 56 | # Ensure the template synthesizes successfully 57 | Template.from_stack(test_stack) 58 | 59 | 60 | def test_techniques_bedrock_flows_conditional_chain_stack_synthesizes_properly(): 61 | app = cdk.App() 62 | 63 | test_stack = FlowsConditionalChain( 64 | app, 65 | "TestStack", 66 | ) 67 | 68 | # Ensure the template synthesizes successfully 69 | Template.from_stack(test_stack) 70 | 71 | 72 | def test_techniques_bedrock_flows_map_chain_stack_synthesizes_properly(): 73 | app = cdk.App() 74 | 75 | test_stack = FlowsMapChain( 76 | app, 77 | "TestStack", 78 | ) 79 | 80 | # Ensure the template synthesizes successfully 81 | Template.from_stack(test_stack) 82 | -------------------------------------------------------------------------------- /techniques_step_functions/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 technique_stacks.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "*.md", 9 | "LICENSE", 10 | "cdk*.json", 11 | "pipeline", 12 | "docs", 13 | "requirements*.txt", 14 | "source.bat", 15 | "**/__init__.py", 16 | "**/__pycache__", 17 | "*test*", 18 | "docker-compose.yml" 19 | ] 20 | }, 21 | "context": { 22 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 23 | "@aws-cdk/core:checkSecretUsage": true, 24 | "@aws-cdk/core:target-partitions": [ 25 | "aws", 26 | "aws-cn" 27 | ], 28 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 29 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/aws-iam:minimizePolicies": true, 32 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 33 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 34 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 35 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 36 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 37 | "@aws-cdk/core:enablePartitionLiterals": true, 38 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 39 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 40 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 41 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 42 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 43 | "@aws-cdk/aws-route53-patters:useCertificate": true, 44 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 45 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 46 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 47 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 48 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 49 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 50 | "@aws-cdk/aws-redshift:columnId": true, 51 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 52 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 53 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 54 | "@aws-cdk/aws-kms:aliasNameRef": true, 55 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 56 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 57 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /techniques_step_functions/functions/parse_json_response/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/techniques_step_functions/functions/parse_json_response/__init__.py -------------------------------------------------------------------------------- /techniques_step_functions/functions/parse_json_response/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | from jsonschema import validate 3 | 4 | 5 | # Parse the JSON response string into an object and validate it against the JSON schema. 6 | # Return the validated object. 7 | def handler(event, context): 8 | response_string = event["response_string"] 9 | response_object = json.loads(response_string) 10 | 11 | json_schema = event["json_schema"] 12 | validate(instance=response_object, schema=json_schema) 13 | 14 | return response_object 15 | -------------------------------------------------------------------------------- /techniques_step_functions/functions/parse_json_response/requirements.txt: -------------------------------------------------------------------------------- 1 | jsonschema==4.24.0 2 | -------------------------------------------------------------------------------- /techniques_step_functions/run-test-execution.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # ParallelChain, SequentialChain, etc 6 | DEMO_NAME=$1 7 | 8 | AWS_ACCOUNT_ID=`aws sts get-caller-identity --query Account --output text` 9 | 10 | EXECUTION_NAME=local-test-`uuidgen` 11 | 12 | echo "Starting execution $EXECUTION_NAME for state machine Techniques-$DEMO_NAME" 13 | aws stepfunctions start-execution \ 14 | --region us-west-2 \ 15 | --name $EXECUTION_NAME \ 16 | --state-machine-arn arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:stateMachine:Techniques-$DEMO_NAME \ 17 | --input file://test-inputs/$DEMO_NAME.json 18 | 19 | echo -e "\nWatch the execution at:" 20 | echo "https://us-west-2.console.aws.amazon.com/states/home?region=us-west-2#/v2/executions/details/arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:execution:Techniques-$DEMO_NAME:$EXECUTION_NAME" 21 | 22 | echo -ne "\nWaiting for execution to complete..." 23 | while true; do 24 | STATUS=`aws stepfunctions describe-execution --region us-west-2 --query status --output text --execution-arn arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:execution:Techniques-$DEMO_NAME:$EXECUTION_NAME` 25 | if [ "$STATUS" = "SUCCEEDED" ] || [ "$STATUS" = "FAILED" ] || [ "$STATUS" = "TIMED_OUT" ] || [ "$STATUS" = "ABORTED" ]; then 26 | echo -e "\n\nExecution completed. Status is $STATUS" 27 | if [ "$STATUS" = "SUCCEEDED" ]; then 28 | echo "Output:" 29 | aws stepfunctions describe-execution \ 30 | --execution-arn arn:aws:states:us-west-2:$AWS_ACCOUNT_ID:execution:Techniques-$DEMO_NAME:$EXECUTION_NAME \ 31 | --query output \ 32 | --output text | jq 33 | exit 0 34 | fi 35 | exit 1 36 | fi 37 | sleep 2 38 | echo -n '.' 39 | done 40 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/aws_service_invocation.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_sns as sns, 5 | aws_stepfunctions as sfn, 6 | aws_stepfunctions_tasks as tasks, 7 | ) 8 | from constructs import Construct 9 | 10 | 11 | class AwsServiceInvocationChain(Stack): 12 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 13 | super().__init__(scope, construct_id, **kwargs) 14 | 15 | get_summary = tasks.BedrockInvokeModel( 16 | self, 17 | "Generate Book Summary", 18 | # Choose the model to invoke 19 | model=bedrock.FoundationModel.from_foundation_model_id( 20 | self, 21 | "Model", 22 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 23 | ), 24 | # Provide the input to the model, including the templated prompt and inference properties 25 | body=sfn.TaskInput.from_object( 26 | { 27 | "anthropic_version": "bedrock-2023-05-31", 28 | "messages": [ 29 | { 30 | "role": "user", 31 | "content": [ 32 | { 33 | "type": "text", 34 | # The prompt is templated with the novel name as variable input. 35 | # The input to the Step Functions execution could be: 36 | # "Pride and Prejudice" 37 | "text": sfn.JsonPath.format( 38 | "Write a 1-2 sentence summary for the book {}.", 39 | sfn.JsonPath.string_at("$$.Execution.Input"), 40 | ), 41 | } 42 | ], 43 | } 44 | ], 45 | "max_tokens": 250, 46 | "temperature": 1, 47 | } 48 | ), 49 | ) 50 | 51 | topic = sns.Topic( 52 | self, "Topic", display_name="Notifications about generated book summaries" 53 | ) 54 | notify_me = tasks.SnsPublish( 55 | self, 56 | "Notify Me", 57 | topic=topic, 58 | message=sfn.TaskInput.from_object( 59 | { 60 | "summary": sfn.JsonPath.string_at("$.Body.content[0].text"), 61 | "book": sfn.JsonPath.string_at("$$.Execution.Input"), 62 | } 63 | ), 64 | result_path=sfn.JsonPath.DISCARD, 65 | output_path="$.Body.content[0].text", 66 | ) 67 | 68 | chain = get_summary.next(notify_me) 69 | 70 | sfn.StateMachine( 71 | self, 72 | "AwsServiceInvocationExample", 73 | state_machine_name="Techniques-AwsServiceInvocation", 74 | definition_body=sfn.DefinitionBody.from_chainable(chain), 75 | ) 76 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/conditional_chain.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_stepfunctions as sfn, 5 | aws_stepfunctions_tasks as tasks, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class ConditionalChain(Stack): 11 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | validate_input = tasks.BedrockInvokeModel( 15 | self, 16 | "Decide if input is a book", 17 | model=bedrock.FoundationModel.from_foundation_model_id( 18 | self, 19 | "Model", 20 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 21 | ), 22 | body=sfn.TaskInput.from_object( 23 | { 24 | "anthropic_version": "bedrock-2023-05-31", 25 | "messages": [ 26 | { 27 | "role": "user", 28 | "content": [ 29 | { 30 | "type": "text", 31 | # As the model if the input is a book or not 32 | "text": sfn.JsonPath.format( 33 | """Does the following text in XML tags refer to the name of a book? 34 | 35 | {} 36 | 37 | Start your response with an explanation of your reasoning, then provide a single 'yes' or 'no' indicating whether the text refers to a book. 38 | 39 | Your response should be formatted as a JSON object. 40 | An example of a valid response is below when the text does refer to a book, inside XML tags. 41 | 42 | \{ 43 | "reasoning": "Brief reasons for why I believe the text refers to a book...", 44 | "is_book": "yes" 45 | \} 46 | 47 | 48 | Another example of a valid response is below when the text does NOT refer to a book, inside XML tags. 49 | 50 | \{ 51 | "reasoning": "Brief reasons for why I believe the text does not refer to a book...", 52 | "is_book": "no" 53 | \} 54 | 55 | Do not include any other content other than the JSON object in your response. Do not include any XML tags in your response.""", 56 | sfn.JsonPath.string_at("$$.Execution.Input"), 57 | ), 58 | } 59 | ], 60 | } 61 | ], 62 | "max_tokens": 250, 63 | "temperature": 1, 64 | } 65 | ), 66 | ) 67 | 68 | model_response_to_array = sfn.Pass( 69 | self, 70 | "Parse Model Response", 71 | parameters={ 72 | "decision": sfn.JsonPath.string_to_json( 73 | sfn.JsonPath.string_at("$.Body.content[0].text") 74 | ), 75 | }, 76 | ) 77 | 78 | get_summary = tasks.BedrockInvokeModel( 79 | self, 80 | "Generate Book Summary", 81 | # Choose the model to invoke 82 | model=bedrock.FoundationModel.from_foundation_model_id( 83 | self, 84 | "Model", 85 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 86 | ), 87 | # Provide the input to the model, including the prompt and inference properties 88 | body=sfn.TaskInput.from_object( 89 | { 90 | "anthropic_version": "bedrock-2023-05-31", 91 | "messages": [ 92 | { 93 | "role": "user", 94 | "content": [ 95 | { 96 | "type": "text", 97 | # The main prompt 98 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 99 | } 100 | ], 101 | } 102 | ], 103 | "max_tokens": 250, 104 | "temperature": 1, 105 | } 106 | ), 107 | ) 108 | 109 | write_an_advertisement = tasks.BedrockInvokeModel( 110 | self, 111 | "Generate Book Advertisement", 112 | model=bedrock.FoundationModel.from_foundation_model_id( 113 | self, 114 | "Model", 115 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 116 | ), 117 | body=sfn.TaskInput.from_object( 118 | { 119 | "anthropic_version": "bedrock-2023-05-31", 120 | # Inject the previous output from the model as past conversation, 121 | # then add the new prompt that relies on previous output as context. 122 | "messages": [ 123 | { 124 | "role": "user", 125 | "content": [ 126 | { 127 | "type": "text", 128 | # The previous step's prompt. 129 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 130 | }, 131 | ], 132 | }, 133 | { 134 | # The previous step's model output 135 | "role": sfn.JsonPath.string_at("$.Body.role"), 136 | "content": sfn.JsonPath.string_at("$.Body.content"), 137 | }, 138 | { 139 | "role": "user", 140 | "content": [ 141 | { 142 | "type": "text", 143 | # The new prompt 144 | "text": "Now write a short advertisement for the novel.", 145 | }, 146 | ], 147 | }, 148 | ], 149 | "max_tokens": 250, 150 | "temperature": 1, 151 | } 152 | ), 153 | # Extract the final response from the model as the result of the Step Functions execution 154 | output_path="$.Body.content[0].text", 155 | ) 156 | 157 | # Chain the steps together with a condition 158 | book_decision = ( 159 | sfn.Choice(self, "Is it a book?") 160 | .when( 161 | sfn.Condition.string_equals("$.decision.is_book", "yes"), 162 | get_summary.next(write_an_advertisement), 163 | ) 164 | .otherwise(sfn.Fail(self, "Input was not a book")) 165 | ) 166 | chain = validate_input.next(model_response_to_array).next(book_decision) 167 | 168 | sfn.StateMachine( 169 | self, 170 | "ConditionalChainExample", 171 | state_machine_name="Techniques-ConditionalChain", 172 | definition_body=sfn.DefinitionBody.from_chainable(chain), 173 | ) 174 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/human_input_chain.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Duration, 3 | Stack, 4 | aws_bedrock as bedrock, 5 | aws_sns as sns, 6 | aws_stepfunctions as sfn, 7 | aws_stepfunctions_tasks as tasks, 8 | ) 9 | from constructs import Construct 10 | 11 | 12 | class HumanInputChain(Stack): 13 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 14 | super().__init__(scope, construct_id, **kwargs) 15 | 16 | get_advertisement = tasks.BedrockInvokeModel( 17 | self, 18 | "Generate Advertisement", 19 | # Choose the model to invoke 20 | model=bedrock.FoundationModel.from_foundation_model_id( 21 | self, 22 | "Model", 23 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 24 | ), 25 | # Provide the input to the model, including the templated prompt and inference properties 26 | body=sfn.TaskInput.from_object( 27 | { 28 | "anthropic_version": "bedrock-2023-05-31", 29 | "messages": [ 30 | { 31 | "role": "user", 32 | "content": [ 33 | { 34 | "type": "text", 35 | # The prompt is templated with the novel name as variable input. 36 | # The input to the Step Functions execution could be: 37 | # "Pride and Prejudice" 38 | "text": sfn.JsonPath.format( 39 | "Write a short advertisement for the book {}.", 40 | sfn.JsonPath.string_at("$$.Execution.Input"), 41 | ), 42 | } 43 | ], 44 | } 45 | ], 46 | "max_tokens": 250, 47 | "temperature": 1, 48 | } 49 | ), 50 | ) 51 | 52 | # Send the generated advertisement to a SNS topic. 53 | # The human receiving the notification is expected to approve or reject the advertisement. 54 | # The human's decision should be sent to the Step Functions execution using the task token. 55 | # aws stepfunctions send-task-success --task-output "{\"decision\": \"yes\"}" --task-token "AQB8A..." 56 | topic = sns.Topic( 57 | self, "Topic", display_name="Human input topic for techniques example" 58 | ) 59 | publish_ad_for_approval = tasks.SnsPublish( 60 | self, 61 | "Get Approval For Advertisement", 62 | topic=topic, 63 | integration_pattern=sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, 64 | message=sfn.TaskInput.from_object( 65 | { 66 | "advertisement": sfn.JsonPath.string_at("$.Body.content[0].text"), 67 | "task_token": sfn.JsonPath.task_token, 68 | } 69 | ), 70 | result_path="$.human_input", 71 | ) 72 | 73 | extract_ad = sfn.Pass( 74 | self, 75 | "Extract Advertisement", 76 | parameters={ 77 | "advertisement": sfn.JsonPath.string_at("$.Body.content[0].text"), 78 | }, 79 | ) 80 | 81 | handle_user_decision = ( 82 | sfn.Choice(self, "Is Advertisement Approved?") 83 | .when( 84 | # Human approved the ad - finish the Step Functions execution 85 | sfn.Condition.string_equals("$.human_input.decision", "yes"), 86 | extract_ad, 87 | ) 88 | .when( 89 | # Human rejected the ad - loop back to generate a new ad 90 | sfn.Condition.string_equals("$.human_input.decision", "no"), 91 | get_advertisement, 92 | ) 93 | .otherwise( 94 | sfn.Fail( 95 | self, 96 | "Invalid Advertisement Approval Value", 97 | cause="Unknown user choice (decision must be yes or no)", 98 | error="Unknown user choice (decision must be yes or no)", 99 | ) 100 | ) 101 | ) 102 | 103 | chain = get_advertisement.next(publish_ad_for_approval).next( 104 | handle_user_decision 105 | ) 106 | 107 | sfn.StateMachine( 108 | self, 109 | "HumanInputExample", 110 | state_machine_name="Techniques-HumanInput", 111 | definition_body=sfn.DefinitionBody.from_chainable(chain), 112 | timeout=Duration.minutes(10), 113 | ) 114 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/map_chain.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_stepfunctions as sfn, 5 | aws_stepfunctions_tasks as tasks, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class MapChain(Stack): 11 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | # Generate a JSON array of book titles and authors 15 | get_books = tasks.BedrockInvokeModel( 16 | self, 17 | "Generate Books Array", 18 | model=bedrock.FoundationModel.from_foundation_model_id( 19 | self, 20 | "Model", 21 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 22 | ), 23 | # Provide the input to the model, including the prompt and inference properties 24 | body=sfn.TaskInput.from_object( 25 | { 26 | "anthropic_version": "bedrock-2023-05-31", 27 | "messages": [ 28 | { 29 | "role": "user", 30 | "content": [ 31 | { 32 | "type": "text", 33 | # The main prompt 34 | "text": """Give me the titles and authors of 5 famous novels. 35 | Your response should be formatted as a JSON array, with each element in the array containing a "title" key for the novel's title and an "author" key with the novel's author. 36 | An example of a valid response is below, inside XML tags. 37 | 38 | [ 39 | \{ 40 | "title": "Title 1", 41 | "author": "Author 1" 42 | \}, 43 | \{ 44 | "title": "Title 2", 45 | "author": "Author 2" 46 | \} 47 | ] 48 | 49 | Do not include any other content other than the JSON object in your response. Do not include any XML tags in your response.""", 50 | } 51 | ], 52 | } 53 | ], 54 | "max_tokens": 250, 55 | "temperature": 1, 56 | } 57 | ), 58 | ) 59 | 60 | model_response_to_array = sfn.Pass( 61 | self, 62 | "Parse Model Response", 63 | parameters={ 64 | "novels": sfn.JsonPath.string_to_json( 65 | sfn.JsonPath.string_at("$.Body.content[0].text") 66 | ), 67 | }, 68 | ) 69 | 70 | get_summary = tasks.BedrockInvokeModel( 71 | self, 72 | "Generate Novel Summary", 73 | model=bedrock.FoundationModel.from_foundation_model_id( 74 | self, 75 | "Model", 76 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 77 | ), 78 | body=sfn.TaskInput.from_object( 79 | { 80 | "anthropic_version": "bedrock-2023-05-31", 81 | "messages": [ 82 | { 83 | "role": "user", 84 | "content": [ 85 | { 86 | "type": "text", 87 | # The prompt is templated with the novel name as variable input, 88 | # which is provided by the previous step that generates a list of novels. 89 | # The input to the task could be: 90 | # { 91 | # "title": "Pride and Prejudice", 92 | # "author": "Jane Austen" 93 | # } 94 | "text": sfn.JsonPath.format( 95 | "Write a 1-2 sentence summary for the novel {} by {}.", 96 | sfn.JsonPath.string_at( 97 | "$.novel.title", 98 | ), 99 | sfn.JsonPath.string_at( 100 | "$.novel.author", 101 | ), 102 | ), 103 | } 104 | ], 105 | } 106 | ], 107 | "max_tokens": 250, 108 | "temperature": 1, 109 | } 110 | ), 111 | ) 112 | 113 | write_an_advertisement = tasks.BedrockInvokeModel( 114 | self, 115 | "Generate Bookstore Advertisement", 116 | model=bedrock.FoundationModel.from_foundation_model_id( 117 | self, 118 | "Model", 119 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 120 | ), 121 | body=sfn.TaskInput.from_object( 122 | { 123 | "anthropic_version": "bedrock-2023-05-31", 124 | "messages": [ 125 | { 126 | "role": "user", 127 | "content": [ 128 | { 129 | # Inject the previous model output summarizing 5 novels into this prompt. 130 | "type": "text", 131 | "text": sfn.JsonPath.format( 132 | """Write a short advertisement for a bookstore that sells the following novels. 133 | 1. {} 134 | 2. {} 135 | 3. {} 136 | 4. {} 137 | 5. {}""", 138 | sfn.JsonPath.string_at( 139 | "$[0].Body.content[0].text" 140 | ), 141 | sfn.JsonPath.string_at( 142 | "$[1].Body.content[0].text" 143 | ), 144 | sfn.JsonPath.string_at( 145 | "$[2].Body.content[0].text" 146 | ), 147 | sfn.JsonPath.string_at( 148 | "$[3].Body.content[0].text" 149 | ), 150 | sfn.JsonPath.string_at( 151 | "$[4].Body.content[0].text" 152 | ), 153 | ), 154 | }, 155 | ], 156 | }, 157 | ], 158 | "max_tokens": 250, 159 | "temperature": 1, 160 | } 161 | ), 162 | # Extract the final response from the model as the result of the Step Functions execution 163 | output_path="$.Body.content[0].text", 164 | ) 165 | 166 | # Hook the agents together into a workflow that contains a map 167 | chain = ( 168 | get_books.next(model_response_to_array) 169 | .next( 170 | sfn.Map( 171 | self, 172 | "Loop Through Novels", 173 | items_path=sfn.JsonPath.string_at("$.novels"), 174 | parameters={ 175 | "novel.$": "$$.Map.Item.Value", 176 | }, 177 | max_concurrency=1, 178 | ).iterator(get_summary) 179 | ) 180 | .next(write_an_advertisement) 181 | ) 182 | 183 | sfn.StateMachine( 184 | self, 185 | "MapExample", 186 | state_machine_name="Techniques-Map", 187 | definition_body=sfn.DefinitionBody.from_chainable(chain), 188 | ) 189 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/model_invocation.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_stepfunctions as sfn, 5 | aws_stepfunctions_tasks as tasks, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class ModelInvocation(Stack): 11 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | get_summary = tasks.BedrockInvokeModel( 15 | self, 16 | "Generate Book Summary", 17 | # Choose the model to invoke 18 | model=bedrock.FoundationModel.from_foundation_model_id( 19 | self, 20 | "Model", 21 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 22 | ), 23 | # Provide the input to the model, including the prompt and inference properties 24 | body=sfn.TaskInput.from_object( 25 | { 26 | "anthropic_version": "bedrock-2023-05-31", 27 | "messages": [ 28 | { 29 | "role": "user", 30 | "content": [ 31 | { 32 | "type": "text", 33 | # The main prompt 34 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 35 | } 36 | ], 37 | } 38 | ], 39 | "max_tokens": 250, 40 | "temperature": 1, 41 | } 42 | ), 43 | # Extract the response from the model as the result of the Step Functions execution 44 | output_path="$.Body.content[0].text", 45 | ) 46 | 47 | sfn.StateMachine( 48 | self, 49 | "ModelInvocationExample", 50 | state_machine_name="Techniques-ModelInvocation", 51 | definition_body=sfn.DefinitionBody.from_chainable(get_summary), 52 | ) 53 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/parallel_chain.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_stepfunctions as sfn, 5 | aws_stepfunctions_tasks as tasks, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class ParallelChain(Stack): 11 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | get_summary = tasks.BedrockInvokeModel( 15 | self, 16 | "Generate Book Summary", 17 | # Choose the model to invoke 18 | model=bedrock.FoundationModel.from_foundation_model_id( 19 | self, 20 | "Model", 21 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 22 | ), 23 | # Provide the input to the model, including the prompt and inference properties 24 | body=sfn.TaskInput.from_object( 25 | { 26 | "anthropic_version": "bedrock-2023-05-31", 27 | "messages": [ 28 | { 29 | "role": "user", 30 | "content": [ 31 | { 32 | "type": "text", 33 | # The main prompt 34 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 35 | } 36 | ], 37 | } 38 | ], 39 | "max_tokens": 250, 40 | "temperature": 1, 41 | } 42 | ), 43 | ) 44 | 45 | get_target_audience = tasks.BedrockInvokeModel( 46 | self, 47 | "Generate Book's Target Audience", 48 | # Choose the model to invoke 49 | model=bedrock.FoundationModel.from_foundation_model_id( 50 | self, 51 | "Model", 52 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 53 | ), 54 | # Provide the input to the model, including the prompt and inference properties 55 | body=sfn.TaskInput.from_object( 56 | { 57 | "anthropic_version": "bedrock-2023-05-31", 58 | "messages": [ 59 | { 60 | "role": "user", 61 | "content": [ 62 | { 63 | "type": "text", 64 | # The main prompt 65 | "text": "Describe the target audience for the book Pride & Prejudice.", 66 | } 67 | ], 68 | } 69 | ], 70 | "max_tokens": 250, 71 | "temperature": 1, 72 | } 73 | ), 74 | ) 75 | 76 | write_an_advertisement = tasks.BedrockInvokeModel( 77 | self, 78 | "Write Book Advertisement", 79 | model=bedrock.FoundationModel.from_foundation_model_id( 80 | self, 81 | "Model", 82 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 83 | ), 84 | body=sfn.TaskInput.from_object( 85 | { 86 | "anthropic_version": "bedrock-2023-05-31", 87 | # Inject the previous output from the model as past conversation, 88 | # then add the new prompt that relies on previous output as context. 89 | "messages": [ 90 | { 91 | "role": "user", 92 | "content": [ 93 | { 94 | "type": "text", 95 | # The previous step's prompt. 96 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 97 | }, 98 | ], 99 | }, 100 | { 101 | # The previous step's model output 102 | "role": sfn.JsonPath.string_at("$.summary.Body.role"), 103 | "content": sfn.JsonPath.string_at("$.summary.Body.content"), 104 | }, 105 | { 106 | "role": "user", 107 | "content": [ 108 | { 109 | "type": "text", 110 | # The previous step's prompt. 111 | "text": "Describe the target audience for the book Pride & Prejudice.", 112 | }, 113 | ], 114 | }, 115 | { 116 | # The previous step's model output 117 | "role": sfn.JsonPath.string_at("$.audience.Body.role"), 118 | "content": sfn.JsonPath.string_at( 119 | "$.audience.Body.content" 120 | ), 121 | }, 122 | { 123 | "role": "user", 124 | "content": [ 125 | { 126 | "type": "text", 127 | # The new prompt 128 | "text": "Now write a short advertisement for the novel.", 129 | }, 130 | ], 131 | }, 132 | ], 133 | "max_tokens": 250, 134 | "temperature": 1, 135 | } 136 | ), 137 | # Extract the final response from the model as the result of the Step Functions execution 138 | output_path="$.Body.content[0].text", 139 | ) 140 | 141 | # Hook the steps together into a chain that contains some parallel steps 142 | chain = ( 143 | sfn.Parallel( 144 | self, 145 | "Parallel Tasks", 146 | result_selector={ 147 | "summary.$": "$[0]", 148 | "audience.$": "$[1]", 149 | }, 150 | ) 151 | .branch(get_summary) 152 | .branch(get_target_audience) 153 | ).next(write_an_advertisement) 154 | 155 | sfn.StateMachine( 156 | self, 157 | "ParallelChainExample", 158 | state_machine_name="Techniques-ParallelChain", 159 | definition_body=sfn.DefinitionBody.from_chainable(chain), 160 | ) 161 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/prompt_templating.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_stepfunctions as sfn, 5 | aws_stepfunctions_tasks as tasks, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class PromptTemplating(Stack): 11 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | get_summary = tasks.BedrockInvokeModel( 15 | self, 16 | "Generate Book Summary", 17 | # Choose the model to invoke 18 | model=bedrock.FoundationModel.from_foundation_model_id( 19 | self, 20 | "Model", 21 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 22 | ), 23 | # Provide the input to the model, including the templated prompt and inference properties 24 | body=sfn.TaskInput.from_object( 25 | { 26 | "anthropic_version": "bedrock-2023-05-31", 27 | "messages": [ 28 | { 29 | "role": "user", 30 | "content": [ 31 | { 32 | "type": "text", 33 | # The prompt is templated with the novel name as variable input. 34 | # The input to the Step Functions execution could be: 35 | # "Pride and Prejudice" 36 | "text": sfn.JsonPath.format( 37 | "Write a 1-2 sentence summary for the book {}.", 38 | sfn.JsonPath.string_at("$$.Execution.Input"), 39 | ), 40 | } 41 | ], 42 | } 43 | ], 44 | "max_tokens": 250, 45 | "temperature": 1, 46 | } 47 | ), 48 | # Extract the final response from the model as the result of the Step Functions execution 49 | output_path="$.Body.content[0].text", 50 | ) 51 | 52 | sfn.StateMachine( 53 | self, 54 | "PromptTemplatingExample", 55 | state_machine_name="Techniques-PromptTemplating", 56 | definition_body=sfn.DefinitionBody.from_chainable(get_summary), 57 | ) 58 | -------------------------------------------------------------------------------- /techniques_step_functions/stacks/sequential_chain.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | Stack, 3 | aws_bedrock as bedrock, 4 | aws_stepfunctions as sfn, 5 | aws_stepfunctions_tasks as tasks, 6 | ) 7 | from constructs import Construct 8 | 9 | 10 | class SequentialChain(Stack): 11 | def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: 12 | super().__init__(scope, construct_id, **kwargs) 13 | 14 | get_summary = tasks.BedrockInvokeModel( 15 | self, 16 | "Generate Book Summary", 17 | # Choose the model to invoke 18 | model=bedrock.FoundationModel.from_foundation_model_id( 19 | self, 20 | "Model", 21 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 22 | ), 23 | # Provide the input to the model, including the prompt and inference properties 24 | body=sfn.TaskInput.from_object( 25 | { 26 | "anthropic_version": "bedrock-2023-05-31", 27 | "messages": [ 28 | { 29 | "role": "user", 30 | "content": [ 31 | { 32 | "type": "text", 33 | # The main prompt 34 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 35 | } 36 | ], 37 | } 38 | ], 39 | "max_tokens": 250, 40 | "temperature": 1, 41 | } 42 | ), 43 | ) 44 | 45 | write_an_advertisement = tasks.BedrockInvokeModel( 46 | self, 47 | "Generate Book Advertisement", 48 | model=bedrock.FoundationModel.from_foundation_model_id( 49 | self, 50 | "Model", 51 | bedrock.FoundationModelIdentifier.ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0, 52 | ), 53 | body=sfn.TaskInput.from_object( 54 | { 55 | "anthropic_version": "bedrock-2023-05-31", 56 | # Inject the previous output from the model as past conversation, 57 | # then add the new prompt that relies on previous output as context. 58 | "messages": [ 59 | { 60 | "role": "user", 61 | "content": [ 62 | { 63 | "type": "text", 64 | # The previous step's prompt. 65 | "text": "Write a 1-2 sentence summary for the book Pride & Prejudice.", 66 | }, 67 | ], 68 | }, 69 | { 70 | # The previous step's model output 71 | "role": sfn.JsonPath.string_at("$.Body.role"), 72 | "content": sfn.JsonPath.string_at("$.Body.content"), 73 | }, 74 | { 75 | "role": "user", 76 | "content": [ 77 | { 78 | "type": "text", 79 | # The new prompt 80 | "text": "Now write a short advertisement for the novel.", 81 | }, 82 | ], 83 | }, 84 | ], 85 | "max_tokens": 250, 86 | "temperature": 1, 87 | } 88 | ), 89 | # Extract the final response from the model as the result of the Step Functions execution 90 | output_path="$.Body.content[0].text", 91 | ) 92 | 93 | # Chain the steps together 94 | chain = get_summary.next(write_an_advertisement) 95 | 96 | sfn.StateMachine( 97 | self, 98 | "SequentialChainExample", 99 | state_machine_name="Techniques-SequentialChain", 100 | definition_body=sfn.DefinitionBody.from_chainable(chain), 101 | ) 102 | -------------------------------------------------------------------------------- /techniques_step_functions/technique_stacks.py: -------------------------------------------------------------------------------- 1 | from aws_cdk import ( 2 | App, 3 | Environment, 4 | ) 5 | from stacks.model_invocation import ModelInvocation 6 | from stacks.prompt_templating import PromptTemplating 7 | from stacks.sequential_chain import SequentialChain 8 | from stacks.parallel_chain import ParallelChain 9 | from stacks.conditional_chain import ConditionalChain 10 | from stacks.human_input_chain import HumanInputChain 11 | from stacks.map_chain import MapChain 12 | from stacks.aws_service_invocation import AwsServiceInvocationChain 13 | from stacks.validation_chain import ValidationChain 14 | import os 15 | 16 | 17 | app = App() 18 | env = Environment(account=os.environ["CDK_DEFAULT_ACCOUNT"], region="us-west-2") 19 | ModelInvocation( 20 | app, 21 | "Techniques-ModelInvocation", 22 | env=env, 23 | ) 24 | PromptTemplating( 25 | app, 26 | "Techniques-PromptTemplating", 27 | env=env, 28 | ) 29 | SequentialChain( 30 | app, 31 | "Techniques-SequentialChain", 32 | env=env, 33 | ) 34 | ParallelChain( 35 | app, 36 | "Techniques-ParallelChain", 37 | env=env, 38 | ) 39 | ConditionalChain( 40 | app, 41 | "Techniques-ConditionalChain", 42 | env=env, 43 | ) 44 | HumanInputChain( 45 | app, 46 | "Techniques-HumanInput", 47 | env=env, 48 | ) 49 | MapChain( 50 | app, 51 | "Techniques-Map", 52 | env=env, 53 | ) 54 | AwsServiceInvocationChain( 55 | app, 56 | "Techniques-AwsServiceInvocation", 57 | env=env, 58 | ) 59 | ValidationChain( 60 | app, 61 | "Techniques-Validation", 62 | env=env, 63 | ) 64 | app.synth() 65 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/AwsServiceInvocation.json: -------------------------------------------------------------------------------- 1 | "Pride and Prejudice" 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/ConditionalChain.json: -------------------------------------------------------------------------------- 1 | "Pride and Prejudice" 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/HumanInput.json: -------------------------------------------------------------------------------- 1 | "Pride and Prejudice" 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/Map.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/ModelInvocation.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/ParallelChain.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/PromptTemplating.json: -------------------------------------------------------------------------------- 1 | "Pride and Prejudice" 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/SequentialChain.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test-inputs/Validation.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /techniques_step_functions/test_step_functions_techniques_stacks.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | from aws_cdk.assertions import Template 3 | 4 | from stacks.model_invocation import ModelInvocation 5 | from stacks.prompt_templating import PromptTemplating 6 | from stacks.sequential_chain import SequentialChain 7 | from stacks.parallel_chain import ParallelChain 8 | from stacks.conditional_chain import ConditionalChain 9 | from stacks.human_input_chain import HumanInputChain 10 | from stacks.map_chain import MapChain 11 | from stacks.aws_service_invocation import ( 12 | AwsServiceInvocationChain, 13 | ) 14 | from stacks.validation_chain import ValidationChain 15 | 16 | 17 | def test_techniques_step_functions_model_invocation_stack_synthesizes_properly(): 18 | app = cdk.App() 19 | 20 | test_stack = ModelInvocation( 21 | app, 22 | "TestStack", 23 | ) 24 | 25 | # Ensure the template synthesizes successfully 26 | Template.from_stack(test_stack) 27 | 28 | 29 | def test_techniques_step_functions_prompt_templating_stack_synthesizes_properly(): 30 | app = cdk.App() 31 | 32 | test_stack = PromptTemplating( 33 | app, 34 | "TestStack", 35 | ) 36 | 37 | # Ensure the template synthesizes successfully 38 | Template.from_stack(test_stack) 39 | 40 | 41 | def test_techniques_step_functions_sequential_chain_stack_synthesizes_properly(): 42 | app = cdk.App() 43 | 44 | test_stack = SequentialChain( 45 | app, 46 | "TestStack", 47 | ) 48 | 49 | # Ensure the template synthesizes successfully 50 | Template.from_stack(test_stack) 51 | 52 | 53 | def test_techniques_step_functions_parallel_chain_stack_synthesizes_properly(): 54 | app = cdk.App() 55 | 56 | test_stack = ParallelChain( 57 | app, 58 | "TestStack", 59 | ) 60 | 61 | # Ensure the template synthesizes successfully 62 | Template.from_stack(test_stack) 63 | 64 | 65 | def test_techniques_step_functions_conditional_chain_stack_synthesizes_properly(): 66 | app = cdk.App() 67 | 68 | test_stack = ConditionalChain( 69 | app, 70 | "TestStack", 71 | ) 72 | 73 | # Ensure the template synthesizes successfully 74 | Template.from_stack(test_stack) 75 | 76 | 77 | def test_techniques_step_functions_human_input_stack_synthesizes_properly(): 78 | app = cdk.App() 79 | 80 | test_stack = HumanInputChain( 81 | app, 82 | "TestStack", 83 | ) 84 | 85 | # Ensure the template synthesizes successfully 86 | Template.from_stack(test_stack) 87 | 88 | 89 | def test_techniques_step_functions_map_chain_stack_synthesizes_properly(): 90 | app = cdk.App() 91 | 92 | test_stack = MapChain( 93 | app, 94 | "TestStack", 95 | ) 96 | 97 | # Ensure the template synthesizes successfully 98 | Template.from_stack(test_stack) 99 | 100 | 101 | def test_techniques_step_functions_service_invocation_chain_stack_synthesizes_properly(): 102 | app = cdk.App() 103 | 104 | test_stack = AwsServiceInvocationChain( 105 | app, 106 | "TestStack", 107 | ) 108 | 109 | # Ensure the template synthesizes successfully 110 | Template.from_stack(test_stack) 111 | 112 | 113 | def test_techniques_step_functions_validation_chain_stack_synthesizes_properly(): 114 | app = cdk.App() 115 | 116 | test_stack = ValidationChain( 117 | app, 118 | "TestStack", 119 | ) 120 | 121 | # Ensure the template synthesizes successfully 122 | Template.from_stack(test_stack) 123 | -------------------------------------------------------------------------------- /test-inputs/BlogPost.json: -------------------------------------------------------------------------------- 1 | { 2 | "novel": "Pride and Prejudice by Jane Austen" 3 | } 4 | -------------------------------------------------------------------------------- /test-inputs/MealPlanner.json: -------------------------------------------------------------------------------- 1 | { 2 | "ingredients": "Chicken, Rice" 3 | } 4 | -------------------------------------------------------------------------------- /test-inputs/MostPopularRepoBedrockAgents.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-inputs/MostPopularRepoLangchain.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test-inputs/MoviePitch.json: -------------------------------------------------------------------------------- 1 | { 2 | "movie_description": "Cowboys in space" 3 | } 4 | -------------------------------------------------------------------------------- /test-inputs/StoryWriter.json: -------------------------------------------------------------------------------- 1 | { 2 | "story_description": "The wild west" 3 | } 4 | -------------------------------------------------------------------------------- /test-inputs/TripPlanner.json: -------------------------------------------------------------------------------- 1 | { 2 | "location": "Paris, France" 3 | } 4 | -------------------------------------------------------------------------------- /test_cdk_stacks.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | from aws_cdk.assertions import Template 3 | 4 | from stacks.webapp_stack import WebappStack 5 | from stacks.blog_post_stack import BlogPostStack 6 | from stacks.trip_planner_stack import TripPlannerStack 7 | from stacks.story_writer_stack import StoryWriterStack 8 | from stacks.movie_pitch_stack import MoviePitchStack 9 | from stacks.meal_planner_stack import MealPlannerStack 10 | from stacks.most_popular_repo_bedrock_agent_stack import ( 11 | MostPopularRepoBedrockAgentStack, 12 | ) 13 | from stacks.most_popular_repo_langchain_stack import ( 14 | MostPopularRepoLangchainStack, 15 | ) 16 | from stacks.alarms_stack import AlarmsStack 17 | 18 | 19 | # Note: the webapp stack and trip planner stack are not tested, because they do account lookups 20 | 21 | 22 | def test_blogpost_stack_synthesizes_properly(): 23 | app = cdk.App() 24 | 25 | test_stack = BlogPostStack( 26 | app, 27 | "TestStack", 28 | ) 29 | 30 | # Ensure the template synthesizes successfully 31 | Template.from_stack(test_stack) 32 | 33 | 34 | def test_storywriter_stack_synthesizes_properly(): 35 | app = cdk.App() 36 | 37 | test_stack = StoryWriterStack( 38 | app, 39 | "TestStack", 40 | ) 41 | 42 | # Ensure the template synthesizes successfully 43 | Template.from_stack(test_stack) 44 | 45 | 46 | def test_moviepitch_stack_synthesizes_properly(): 47 | app = cdk.App() 48 | 49 | test_stack = MoviePitchStack( 50 | app, 51 | "TestStack", 52 | ) 53 | 54 | # Ensure the template synthesizes successfully 55 | Template.from_stack(test_stack) 56 | 57 | 58 | def test_mealplanner_stack_synthesizes_properly(): 59 | app = cdk.App() 60 | 61 | test_stack = MealPlannerStack( 62 | app, 63 | "TestStack", 64 | ) 65 | 66 | # Ensure the template synthesizes successfully 67 | Template.from_stack(test_stack) 68 | 69 | 70 | def test_mostpopularrepo_bedrockagents_stack_synthesizes_properly(): 71 | app = cdk.App() 72 | 73 | test_stack = MostPopularRepoBedrockAgentStack( 74 | app, 75 | "TestStack", 76 | ) 77 | 78 | # Ensure the template synthesizes successfully 79 | Template.from_stack(test_stack) 80 | 81 | 82 | def test_mostpopularrepo_langchain_stack_synthesizes_properly(): 83 | app = cdk.App() 84 | 85 | test_stack = MostPopularRepoLangchainStack( 86 | app, 87 | "TestStack", 88 | ) 89 | 90 | # Ensure the template synthesizes successfully 91 | Template.from_stack(test_stack) 92 | 93 | 94 | def test_alarms_stack_synthesizes_properly(): 95 | app = cdk.App() 96 | 97 | test_stack = AlarmsStack( 98 | app, 99 | "TestStack", 100 | ) 101 | 102 | # Ensure the template synthesizes successfully 103 | Template.from_stack(test_stack) 104 | -------------------------------------------------------------------------------- /webapp/Home.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | 3 | st.set_page_config(layout="wide") 4 | 5 | st.title("Amazon Bedrock Serverless Prompt Chaining Examples") 6 | 7 | """ 8 | This demo application provides examples of using [AWS Step Functions](https://aws.amazon.com/step-functions/) 9 | and [Amazon Bedrock](https://aws.amazon.com/bedrock/) to build complex, serverless, and highly scalable 10 | generative AI applications with prompt chaining. 11 | 12 | The source code for these demos can be found at 13 | [on GitHub](https://github.com/aws-samples/amazon-bedrock-serverless-prompt-chaining). 14 | """ 15 | -------------------------------------------------------------------------------- /webapp/pages/1_Blog_Post.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import json 4 | 5 | import stepfn 6 | 7 | st.set_page_config(layout="wide") 8 | 9 | st.title("Blog post generator demo") 10 | 11 | execution_status_container = None 12 | 13 | # Populate a unique user ID to use for naming the Step Functions execution 14 | if "user_id" not in st.session_state: 15 | st.session_state.user_id = str(uuid.uuid4()) 16 | 17 | 18 | def display_state_machine_status(status_markdown): 19 | if execution_status_container: 20 | execution_status_container.empty() 21 | with execution_status_container.container(): 22 | st.subheader("⚙️ Step Functions execution") 23 | st.markdown(status_markdown) 24 | 25 | 26 | def display_no_state_machine_status(): 27 | if execution_status_container: 28 | execution_status_container.empty() 29 | with execution_status_container.container(): 30 | st.subheader("⚙️ Step Functions execution") 31 | st.write("Not started yet.") 32 | 33 | 34 | def execute_state_machine(novel): 35 | input = {"novel": novel} 36 | execution_arn = stepfn.start_execution( 37 | "PromptChainDemo-BlogPost", 38 | st.session_state.user_id, 39 | json.dumps(input), 40 | ) 41 | st.session_state.blog_post_execution_arn = execution_arn 42 | return stepfn.poll_for_execution_completion( 43 | execution_arn, display_state_machine_status 44 | ) 45 | 46 | 47 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 48 | 49 | with behind_the_scenes_col: 50 | execution_status_container = st.empty() 51 | 52 | if "blog_post_execution_arn" in st.session_state: 53 | status_markdown = stepfn.describe_execution( 54 | st.session_state.blog_post_execution_arn 55 | ) 56 | display_state_machine_status(status_markdown) 57 | else: 58 | display_no_state_machine_status() 59 | 60 | st.subheader("🔍 Step Functions state machine") 61 | st.image(image="/app/pages/workflow_images/blog_post.png") 62 | 63 | 64 | with demo_col: 65 | st.subheader("🚀 Demo") 66 | with st.form("start_blog_post_demo_form"): 67 | st.info( 68 | "Press Start to generate a blog post about your provided novel, which will analyze the novel for your literature blog." 69 | ) 70 | novel_text = st.text_input( 71 | "Enter a novel:", "Pride and Prejudice by Jane Austen" 72 | ) 73 | started = st.form_submit_button("Start") 74 | if started: 75 | with st.spinner("Wait for it..."): 76 | if "blog_post_execution_arn" in st.session_state: 77 | del st.session_state["blog_post_execution_arn"] 78 | display_no_state_machine_status() 79 | response = execute_state_machine(novel_text) 80 | 81 | st.session_state.blog_post_execution_status = response["status"] 82 | if response["status"] == "SUCCEEDED": 83 | output = json.loads(response["output"]) 84 | st.session_state.blog_post_content = output 85 | 86 | if st.session_state.blog_post_execution_status == "SUCCEEDED": 87 | st.success("Done!") 88 | st.write(st.session_state.blog_post_content) 89 | else: 90 | st.error("The blog post could not be written. Please try again.") 91 | -------------------------------------------------------------------------------- /webapp/pages/2_Story_Writer.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import json 4 | 5 | import stepfn 6 | 7 | st.set_page_config(layout="wide") 8 | 9 | st.title("Story writer demo") 10 | 11 | execution_status_container = None 12 | 13 | # Populate a unique user ID to use for naming the Step Functions execution 14 | if "user_id" not in st.session_state: 15 | st.session_state.user_id = str(uuid.uuid4()) 16 | 17 | 18 | def display_state_machine_status(status_markdown): 19 | if execution_status_container: 20 | execution_status_container.empty() 21 | with execution_status_container.container(): 22 | st.subheader("⚙️ Step Functions execution") 23 | st.markdown(status_markdown) 24 | 25 | 26 | def display_no_state_machine_status(): 27 | if execution_status_container: 28 | execution_status_container.empty() 29 | with execution_status_container.container(): 30 | st.subheader("⚙️ Step Functions execution") 31 | st.write("Not started yet.") 32 | 33 | 34 | def execute_state_machine(story_description): 35 | input = {"story_description": story_description} 36 | execution_arn = stepfn.start_execution( 37 | "PromptChainDemo-StoryWriter", 38 | st.session_state.user_id, 39 | json.dumps(input), 40 | ) 41 | st.session_state.story_writer_execution_arn = execution_arn 42 | return stepfn.poll_for_execution_completion( 43 | execution_arn, display_state_machine_status 44 | ) 45 | 46 | 47 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 48 | 49 | with behind_the_scenes_col: 50 | execution_status_container = st.empty() 51 | 52 | if "story_writer_execution_arn" in st.session_state: 53 | status_markdown = stepfn.describe_execution( 54 | st.session_state.story_writer_execution_arn 55 | ) 56 | display_state_machine_status(status_markdown) 57 | else: 58 | display_no_state_machine_status() 59 | 60 | st.subheader("🔍 Step Functions state machine") 61 | st.image(image="/app/pages/workflow_images/story_writer.png") 62 | 63 | with demo_col: 64 | st.subheader("🚀 Demo") 65 | 66 | with st.form("start_story_writer_demo_form"): 67 | st.info("Press Start to generate a story about your provided description.") 68 | story_description_text = st.text_input( 69 | "Enter a short description or genre for your story:", "The wild west" 70 | ) 71 | started = st.form_submit_button("Start") 72 | if started: 73 | with st.spinner("Wait for it..."): 74 | if "story_writer_execution_arn" in st.session_state: 75 | del st.session_state["story_writer_execution_arn"] 76 | display_no_state_machine_status() 77 | response = execute_state_machine(story_description_text) 78 | 79 | st.session_state.story_writer_execution_status = response["status"] 80 | if response["status"] == "SUCCEEDED": 81 | output = json.loads(response["output"]) 82 | st.session_state.story = output["story"] 83 | 84 | if st.session_state.story_writer_execution_status == "SUCCEEDED": 85 | st.success("Done!") 86 | st.write(st.session_state.story) 87 | else: 88 | st.error("Your story could not be created. Please try again.") 89 | -------------------------------------------------------------------------------- /webapp/pages/3_Trip_Planner.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import requests 3 | import uuid 4 | import json 5 | 6 | import stepfn 7 | 8 | st.set_page_config(layout="wide") 9 | 10 | st.title("Trip planner demo") 11 | 12 | execution_status_container = None 13 | 14 | # Populate a unique user ID to use for naming the Step Functions execution 15 | if "user_id" not in st.session_state: 16 | st.session_state.user_id = str(uuid.uuid4()) 17 | 18 | 19 | def display_state_machine_status(status_markdown): 20 | if execution_status_container: 21 | execution_status_container.empty() 22 | with execution_status_container.container(): 23 | st.subheader("⚙️ Step Functions execution") 24 | st.markdown(status_markdown) 25 | 26 | 27 | def display_no_state_machine_status(): 28 | if execution_status_container: 29 | execution_status_container.empty() 30 | with execution_status_container.container(): 31 | st.subheader("⚙️ Step Functions execution") 32 | st.write("Not started yet.") 33 | 34 | 35 | def execute_state_machine(location): 36 | input = {"location": location} 37 | execution_arn = stepfn.start_execution( 38 | "PromptChainDemo-TripPlanner", 39 | st.session_state.user_id, 40 | json.dumps(input), 41 | ) 42 | st.session_state.trip_planner_execution_arn = execution_arn 43 | return stepfn.poll_for_execution_completion( 44 | execution_arn, display_state_machine_status 45 | ) 46 | 47 | 48 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 49 | 50 | with behind_the_scenes_col: 51 | execution_status_container = st.empty() 52 | 53 | if "trip_planner_execution_arn" in st.session_state: 54 | status_markdown = stepfn.describe_execution( 55 | st.session_state.trip_planner_execution_arn 56 | ) 57 | display_state_machine_status(status_markdown) 58 | else: 59 | display_no_state_machine_status() 60 | 61 | st.subheader("🔍 Step Functions state machine") 62 | st.image(image="/app/pages/workflow_images/trip_planner.png") 63 | 64 | with demo_col: 65 | st.subheader("🚀 Demo") 66 | 67 | with st.form("start_trip_planner_demo_form"): 68 | st.info("Press Start to plan a weekend vacation to your chosen location.") 69 | location_text = st.text_input( 70 | "Enter a location for your trip:", "Paris, France" 71 | ) 72 | started = st.form_submit_button("Start") 73 | if started: 74 | with st.spinner("Wait for it..."): 75 | if "trip_planner_execution_arn" in st.session_state: 76 | del st.session_state["trip_planner_execution_arn"] 77 | display_no_state_machine_status() 78 | response = execute_state_machine(location_text) 79 | 80 | st.session_state.trip_planner_execution_status = response["status"] 81 | if response["status"] == "SUCCEEDED": 82 | output = json.loads(response["output"]) 83 | pdf_file = requests.get(output["itinerary_url"]) 84 | st.session_state.trip_itinerary = pdf_file.content 85 | 86 | if st.session_state.trip_planner_execution_status == "SUCCEEDED": 87 | st.success("Done! Download your itinerary PDF using the button below.") 88 | else: 89 | st.error("Your trip itinerary could not be created. Please try again.") 90 | 91 | if "trip_itinerary" in st.session_state: 92 | st.download_button( 93 | label="Download itinerary", 94 | data=st.session_state.trip_itinerary, 95 | file_name="itinerary.pdf", 96 | mime="application/pdf", 97 | ) 98 | -------------------------------------------------------------------------------- /webapp/pages/4_Movie_Pitch.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import json 4 | import time 5 | 6 | import stepfn 7 | 8 | st.set_page_config(layout="wide") 9 | 10 | st.title("Movie pitch demo") 11 | 12 | execution_status_container = None 13 | 14 | # Populate a unique user ID to use for naming the Step Functions execution 15 | if "user_id" not in st.session_state: 16 | st.session_state.user_id = str(uuid.uuid4()) 17 | 18 | if "previous_movie_pitches" not in st.session_state: 19 | st.session_state.previous_movie_pitches = [] 20 | 21 | 22 | def display_state_machine_status(status_markdown): 23 | if execution_status_container: 24 | execution_status_container.empty() 25 | with execution_status_container.container(): 26 | st.subheader("⚙️ Step Functions execution") 27 | st.markdown(status_markdown) 28 | 29 | 30 | def display_no_state_machine_status(): 31 | if execution_status_container: 32 | execution_status_container.empty() 33 | with execution_status_container.container(): 34 | st.subheader("⚙️ Step Functions execution") 35 | st.write("Not started yet.") 36 | 37 | 38 | def execute_state_machine(movie_description): 39 | input = {"movie_description": movie_description} 40 | execution_arn = stepfn.start_execution( 41 | "PromptChainDemo-MoviePitch", 42 | st.session_state.user_id, 43 | json.dumps(input), 44 | ) 45 | st.session_state.movie_pitch_execution_arn = execution_arn 46 | return stepfn.poll_for_execution_task_token_or_completion( 47 | execution_arn, display_state_machine_status 48 | ) 49 | 50 | 51 | def continue_state_machine(task_result): 52 | stepfn.continue_execution( 53 | st.session_state.movie_pitch_task_token, {"Payload": task_result} 54 | ) 55 | time.sleep(2) 56 | return stepfn.poll_for_execution_task_token_or_completion( 57 | st.session_state.movie_pitch_execution_arn, display_state_machine_status 58 | ) 59 | 60 | 61 | def handle_stepfn_response(response): 62 | if response["status"] == "SUCCEEDED": 63 | output = json.loads(response["output"]) 64 | st.session_state.movie_pitch_one_pager = output["movie_pitch_one_pager"] 65 | elif response["status"] == "PAUSED": 66 | task_payload = response["task_payload"] 67 | st.session_state.movie_pitch_task_pitch = task_payload["input"]["movie_pitch"] 68 | st.session_state.movie_pitch_task_token = task_payload["token"] 69 | st.session_state.movie_pitch_execution_status = response["status"] 70 | 71 | 72 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 73 | 74 | with behind_the_scenes_col: 75 | execution_status_container = st.empty() 76 | 77 | if "movie_pitch_execution_arn" in st.session_state: 78 | status_markdown = stepfn.describe_execution( 79 | st.session_state.movie_pitch_execution_arn 80 | ) 81 | display_state_machine_status(status_markdown) 82 | else: 83 | display_no_state_machine_status() 84 | 85 | st.subheader("🔍 Step Functions state machine") 86 | st.image(image="/app/pages/workflow_images/movie_pitch.png") 87 | 88 | with demo_col: 89 | st.subheader("🚀 Demo") 90 | 91 | # First, ask for the description or genre of the movie and start generating the movie pitch 92 | if "movie_pitch_description" not in st.session_state: 93 | with st.form("start_movie_pitch_demo_form"): 94 | st.info( 95 | "Press Start to generate a movie pitch from your provided description." 96 | ) 97 | movie_description_text = st.text_input( 98 | "Enter a short description or genre for your movie:", "Cowboys in space" 99 | ) 100 | started = st.form_submit_button("Start") 101 | if started: 102 | with st.spinner("Wait for it..."): 103 | if "movie_pitch_execution_arn" in st.session_state: 104 | del st.session_state["movie_pitch_execution_arn"] 105 | display_no_state_machine_status() 106 | 107 | st.session_state.movie_pitch_description = movie_description_text 108 | response = execute_state_machine(movie_description_text) 109 | handle_stepfn_response(response) 110 | st.experimental_rerun() 111 | else: 112 | with st.expander("Movie description", expanded=True): 113 | st.info("You previously provided the following movie description:") 114 | st.write(st.session_state.movie_pitch_description) 115 | 116 | # Show the previous movie pitches that the user has already greenlit or not 117 | for i, previous_pitch in enumerate(st.session_state.previous_movie_pitches): 118 | with st.expander("Movie pitch #" + str(i + 1), expanded=True): 119 | st.info("The screenwriter previously pitched the following movie to you:") 120 | st.write(previous_pitch["movie_pitch"]) 121 | if previous_pitch["user_choice"] == "yes": 122 | st.success("You chose to greenlight this movie!") 123 | else: 124 | st.warning( 125 | "You chose not to greenlight this movie and to get a new pitch." 126 | ) 127 | 128 | if "movie_pitch_execution_status" in st.session_state: 129 | # If there is a pending movie pitch, ask the user whether they want to greenlight the movie or not 130 | if st.session_state.movie_pitch_execution_status == "PAUSED": 131 | with st.form("decide_on_pitch_form"): 132 | st.info( 133 | "You are a movie producer, and a screenwriter has come to you with the following short pitch." 134 | ) 135 | st.write(st.session_state.movie_pitch_task_pitch) 136 | st.info( 137 | "Do you want to greenlight this movie? If so, a longer one-pager movie pitch will be generated. If not, a new short pitch will be generated." 138 | ) 139 | yes = st.form_submit_button("Yes, greenlight the movie!") 140 | no = st.form_submit_button("No, try again.") 141 | if yes or no: 142 | with st.spinner("Wait for it..."): 143 | result = { 144 | "movie_description": st.session_state.movie_pitch_description, 145 | "movie_pitch": st.session_state.movie_pitch_task_pitch, 146 | "user_choice": "yes" if yes else "no", 147 | } 148 | response = continue_state_machine(result) 149 | st.session_state.previous_movie_pitches.append(result) 150 | handle_stepfn_response(response) 151 | st.experimental_rerun() 152 | 153 | # If the workflow is complete, show the full movie pitch one-pager 154 | elif st.session_state.movie_pitch_execution_status == "SUCCEEDED": 155 | if "movie_pitch_one_pager" in st.session_state: 156 | with st.expander("Movie pitch one-pager", expanded=True): 157 | st.success("Done! I look forward to seeing this movie get made!") 158 | st.write(st.session_state.movie_pitch_one_pager) 159 | 160 | # Workflow failed 161 | else: 162 | st.error("Your movie pitch could not be created. Please try again.") 163 | -------------------------------------------------------------------------------- /webapp/pages/5_Meal_Planner.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import json 4 | 5 | import stepfn 6 | 7 | st.set_page_config(layout="wide") 8 | 9 | st.title("Meal planner demo") 10 | 11 | execution_status_container = None 12 | 13 | # Populate a unique user ID to use for naming the Step Functions execution 14 | if "user_id" not in st.session_state: 15 | st.session_state.user_id = str(uuid.uuid4()) 16 | 17 | 18 | def display_state_machine_status(status_markdown): 19 | if execution_status_container: 20 | execution_status_container.empty() 21 | with execution_status_container.container(): 22 | st.subheader("⚙️ Step Functions execution") 23 | st.markdown(status_markdown) 24 | 25 | 26 | def display_no_state_machine_status(): 27 | if execution_status_container: 28 | execution_status_container.empty() 29 | with execution_status_container.container(): 30 | st.subheader("⚙️ Step Functions execution") 31 | st.write("Not started yet.") 32 | 33 | 34 | def execute_state_machine(ingredients): 35 | input = {"ingredients": ingredients} 36 | execution_arn = stepfn.start_execution( 37 | "PromptChainDemo-MealPlanner", 38 | st.session_state.user_id, 39 | json.dumps(input), 40 | ) 41 | st.session_state.meal_planner_execution_arn = execution_arn 42 | return stepfn.poll_for_execution_completion( 43 | execution_arn, display_state_machine_status 44 | ) 45 | 46 | 47 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 48 | 49 | with behind_the_scenes_col: 50 | execution_status_container = st.empty() 51 | 52 | if "meal_planner_execution_arn" in st.session_state: 53 | status_markdown = stepfn.describe_execution( 54 | st.session_state.meal_planner_execution_arn 55 | ) 56 | display_state_machine_status(status_markdown) 57 | else: 58 | display_no_state_machine_status() 59 | 60 | st.subheader("🔍 Step Functions state machine") 61 | st.image(image="/app/pages/workflow_images/meal_planner.png") 62 | 63 | with demo_col: 64 | st.subheader("🚀 Demo") 65 | 66 | with st.form("start_meal_planner_demo_form"): 67 | st.info( 68 | "Press Start to generate a tasty dinner recipe using your provided ingredients." 69 | ) 70 | ingredients_text = st.text_input( 71 | "Enter a few ingredients you have on hand:", "Chicken, Rice" 72 | ) 73 | started = st.form_submit_button("Start") 74 | if started: 75 | with st.spinner("Wait for it..."): 76 | if "meal_planner_execution_arn" in st.session_state: 77 | del st.session_state["meal_planner_execution_arn"] 78 | display_no_state_machine_status() 79 | response = execute_state_machine(ingredients_text) 80 | 81 | st.session_state.meal_planner_execution_status = response["status"] 82 | if response["status"] == "SUCCEEDED": 83 | output = json.loads(response["output"]) 84 | st.session_state.recipe = output["recipe"] 85 | 86 | if st.session_state.meal_planner_execution_status == "SUCCEEDED": 87 | st.success("Done!") 88 | st.write(st.session_state.recipe) 89 | else: 90 | st.error("Your meal recipe could not be created. Please try again.") 91 | -------------------------------------------------------------------------------- /webapp/pages/6_Most_Popular_Repo_(Bedrock_Agents).py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import json 4 | 5 | import stepfn 6 | 7 | st.set_page_config(layout="wide") 8 | 9 | st.title("Most popular repo demo - Bedrock Agents version") 10 | 11 | execution_status_container = None 12 | 13 | # Populate a unique user ID to use for naming the Step Functions execution 14 | if "user_id" not in st.session_state: 15 | st.session_state.user_id = str(uuid.uuid4()) 16 | 17 | 18 | def display_state_machine_status(status_markdown): 19 | if execution_status_container: 20 | execution_status_container.empty() 21 | with execution_status_container.container(): 22 | st.subheader("⚙️ Step Functions execution") 23 | st.markdown(status_markdown) 24 | 25 | 26 | def display_no_state_machine_status(): 27 | if execution_status_container: 28 | execution_status_container.empty() 29 | with execution_status_container.container(): 30 | st.subheader("⚙️ Step Functions execution") 31 | st.write("Not started yet.") 32 | 33 | 34 | def execute_state_machine(): 35 | execution_arn = stepfn.start_execution( 36 | "PromptChainDemo-MostPopularRepoBedrockAgents", 37 | st.session_state.user_id, 38 | "{}", 39 | ) 40 | st.session_state.most_popular_repo_ba_execution_arn = execution_arn 41 | return stepfn.poll_for_execution_completion( 42 | execution_arn, 43 | display_state_machine_status, 44 | ) 45 | 46 | 47 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 48 | 49 | with behind_the_scenes_col: 50 | execution_status_container = st.empty() 51 | 52 | if "most_popular_repo_ba_execution_arn" in st.session_state: 53 | status_markdown = stepfn.describe_execution( 54 | st.session_state.most_popular_repo_ba_execution_arn 55 | ) 56 | display_state_machine_status(status_markdown) 57 | else: 58 | display_no_state_machine_status() 59 | 60 | st.subheader("🔍 Step Functions state machine") 61 | st.image(image="/app/pages/workflow_images/most_popular_repo.png") 62 | 63 | 64 | with demo_col: 65 | st.subheader("🚀 Demo") 66 | 67 | with st.form("start_most_popular_repo_ba_demo_form"): 68 | st.info( 69 | "Press Start to get information about the highest trending repository on GitHub today." 70 | ) 71 | started = st.form_submit_button("Start") 72 | if started: 73 | with st.spinner("Wait for it..."): 74 | if "most_popular_repo_ba_execution_arn" in st.session_state: 75 | del st.session_state["most_popular_repo_ba_execution_arn"] 76 | display_no_state_machine_status() 77 | response = execute_state_machine() 78 | 79 | st.session_state.most_popular_repo_ba_execution_status = response[ 80 | "status" 81 | ] 82 | if response["status"] == "SUCCEEDED": 83 | output = json.loads(response["output"]) 84 | st.session_state.most_popular_repo_ba_url = output["repo"] 85 | st.session_state.most_popular_repo_ba_summary = output["summary"] 86 | 87 | if st.session_state.most_popular_repo_ba_execution_status == "SUCCEEDED": 88 | st.success("Done!") 89 | st.write( 90 | "The most popular repository on GitHub today is: ", 91 | st.session_state.most_popular_repo_ba_url, 92 | ) 93 | st.write(st.session_state.most_popular_repo_ba_summary) 94 | else: 95 | st.error( 96 | "The most popular repository could not be found. Please try again." 97 | ) 98 | -------------------------------------------------------------------------------- /webapp/pages/6_Most_Popular_Repo_(Langchain).py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import uuid 3 | import json 4 | 5 | import stepfn 6 | 7 | st.set_page_config(layout="wide") 8 | 9 | st.title("Most popular repo demo - Langchain version") 10 | 11 | execution_status_container = None 12 | 13 | # Populate a unique user ID to use for naming the Step Functions execution 14 | if "user_id" not in st.session_state: 15 | st.session_state.user_id = str(uuid.uuid4()) 16 | 17 | 18 | def display_state_machine_status(status_markdown): 19 | if execution_status_container: 20 | execution_status_container.empty() 21 | with execution_status_container.container(): 22 | st.subheader("⚙️ Step Functions execution") 23 | st.markdown(status_markdown) 24 | 25 | 26 | def display_no_state_machine_status(): 27 | if execution_status_container: 28 | execution_status_container.empty() 29 | with execution_status_container.container(): 30 | st.subheader("⚙️ Step Functions execution") 31 | st.write("Not started yet.") 32 | 33 | 34 | def execute_state_machine(): 35 | execution_arn = stepfn.start_execution( 36 | "PromptChainDemo-MostPopularRepoLangchain", 37 | st.session_state.user_id, 38 | "{}", 39 | ) 40 | st.session_state.most_popular_repo_execution_arn = execution_arn 41 | return stepfn.poll_for_execution_completion( 42 | execution_arn, display_state_machine_status 43 | ) 44 | 45 | 46 | demo_col, behind_the_scenes_col = st.columns(spec=[1, 1], gap="large") 47 | 48 | with behind_the_scenes_col: 49 | execution_status_container = st.empty() 50 | 51 | if "most_popular_repo_execution_arn" in st.session_state: 52 | status_markdown = stepfn.describe_execution( 53 | st.session_state.most_popular_repo_execution_arn 54 | ) 55 | display_state_machine_status(status_markdown) 56 | else: 57 | display_no_state_machine_status() 58 | 59 | st.subheader("🔍 Step Functions state machine") 60 | st.image(image="/app/pages/workflow_images/most_popular_repo.png") 61 | 62 | 63 | with demo_col: 64 | st.subheader("🚀 Demo") 65 | 66 | with st.form("start_most_popular_repo_demo_form"): 67 | st.info( 68 | "Press Start to get information about the highest trending repository on GitHub today." 69 | ) 70 | started = st.form_submit_button("Start") 71 | if started: 72 | with st.spinner("Wait for it..."): 73 | if "most_popular_repo_execution_arn" in st.session_state: 74 | del st.session_state["most_popular_repo_execution_arn"] 75 | display_no_state_machine_status() 76 | response = execute_state_machine() 77 | 78 | st.session_state.most_popular_repo_execution_status = response["status"] 79 | if response["status"] == "SUCCEEDED": 80 | output = json.loads(response["output"]) 81 | st.session_state.most_popular_repo_url = output["repo"] 82 | st.session_state.most_popular_repo_summary = output["summary"] 83 | 84 | if st.session_state.most_popular_repo_execution_status == "SUCCEEDED": 85 | st.success("Done!") 86 | st.write( 87 | "The most popular repository on GitHub today is: ", 88 | st.session_state.most_popular_repo_url, 89 | ) 90 | st.write(st.session_state.most_popular_repo_summary) 91 | else: 92 | st.error( 93 | "The most popular repository could not be found. Please try again." 94 | ) 95 | -------------------------------------------------------------------------------- /webapp/pages/workflow_images/blog_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/webapp/pages/workflow_images/blog_post.png -------------------------------------------------------------------------------- /webapp/pages/workflow_images/meal_planner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/webapp/pages/workflow_images/meal_planner.png -------------------------------------------------------------------------------- /webapp/pages/workflow_images/most_popular_repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/webapp/pages/workflow_images/most_popular_repo.png -------------------------------------------------------------------------------- /webapp/pages/workflow_images/movie_pitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/webapp/pages/workflow_images/movie_pitch.png -------------------------------------------------------------------------------- /webapp/pages/workflow_images/story_writer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/webapp/pages/workflow_images/story_writer.png -------------------------------------------------------------------------------- /webapp/pages/workflow_images/trip_planner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-bedrock-serverless-prompt-chaining/6c3a7b0c2672593d6f2e0844af6cf4aa8aa1c9a1/webapp/pages/workflow_images/trip_planner.png -------------------------------------------------------------------------------- /webapp/requirements.txt: -------------------------------------------------------------------------------- 1 | asyncio 2 | boto3==1.38.27 3 | streamlit>=1.25.0 4 | -------------------------------------------------------------------------------- /webapp/stepfn.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | import time 4 | import uuid 5 | 6 | 7 | sfn_client = boto3.client("stepfunctions") 8 | sts_client = boto3.client("sts") 9 | default_region = boto3.session.Session().region_name 10 | 11 | 12 | # Methods for displaying the state machine's execution history 13 | def find_task_id(event, events_by_id): 14 | # The "Task" is the event where we first see the "Entered" state and which has a name 15 | if event["type"] == "WaitStateEntered": 16 | return { 17 | "task_id": event["id"], 18 | "task_name": "Wait a few seconds to avoid Bedrock throttling", 19 | } 20 | elif ( 21 | "stateEnteredEventDetails" in event 22 | and "name" in event["stateEnteredEventDetails"] 23 | ): 24 | return { 25 | "task_id": event["id"], 26 | "task_name": event["stateEnteredEventDetails"]["name"], 27 | } 28 | else: 29 | # Go back through the event history until we find the original TaskStateEntered/WaitStateEntered event for this task 30 | previous_event_id = event["previousEventId"] 31 | if previous_event_id not in events_by_id: 32 | raise Exception( 33 | f"Could not find previous event {previous_event_id} for event {event['id']}" 34 | ) 35 | return find_task_id(events_by_id[previous_event_id], events_by_id) 36 | 37 | 38 | known_event_types = [ 39 | "TaskStateEntered", 40 | "TaskScheduled", 41 | "TaskStarted", 42 | "TaskStartFailed", 43 | "TaskFailed", 44 | "TaskTimedOut", 45 | "TaskSucceeded", 46 | "WaitStateEntered", 47 | "WaitStateExited", 48 | ] 49 | 50 | 51 | def get_task_status(event_type): 52 | if ( 53 | event_type == "TaskStateEntered" 54 | or event_type == "TaskScheduled" 55 | or event_type == "TaskStarted" 56 | or event_type == "WaitStateEntered" 57 | ): 58 | return ":arrows_counterclockwise:" 59 | elif ( 60 | event_type == "TaskStartFailed" 61 | or event_type == "TaskFailed" 62 | or event_type == "TaskTimedOut" 63 | ): 64 | return ":bangbang:" 65 | elif event_type == "TaskSucceeded" or event_type == "WaitStateExited": 66 | return ":white_check_mark:" 67 | raise Exception(f"Unknown event type {event_type}") 68 | 69 | 70 | def get_workflow_status_icon(status): 71 | if status == "RUNNING": 72 | return ":arrows_counterclockwise:" 73 | elif status == "FAILED" or status == "TIMED_OUT" or status == "ABORTED": 74 | return ":bangbang:" 75 | elif status == "SUCCEEDED": 76 | return ":white_check_mark:" 77 | raise Exception(f"Unknown event type {status}") 78 | 79 | 80 | def get_workflow_status_markdown(execution, execution_events): 81 | markdown = f"##### Status: {get_workflow_status_icon(execution['status'])} {execution['status'].title()}" 82 | markdown += f"\n\n##### Tasks" 83 | 84 | # Keep a dictionary of events: event ID -> event details 85 | events_by_id = {} 86 | for event in execution_events: 87 | events_by_id[event["id"]] = event 88 | 89 | # Keep a dictionary of tasks: task ID -> state (running, failed, succeeded) and name 90 | task_status = {} 91 | 92 | # Determine the state of each unique task and its unique task name 93 | for event in execution_events: 94 | if event["type"] not in known_event_types: 95 | continue 96 | task = find_task_id(event, events_by_id) 97 | status = get_task_status(event["type"]) 98 | task_status[task["task_id"]] = { 99 | "task_id": task["task_id"], 100 | "task_name": task["task_name"], 101 | "task_status": status, 102 | } 103 | 104 | # Display the task status 105 | task_ids = list(task_status.keys()) 106 | task_ids.sort() 107 | for task_id in task_ids: 108 | task = task_status[task_id] 109 | markdown += f"\n\n{task['task_status']} {task['task_name'].replace(' (Invoke Model)', '')}" 110 | 111 | return markdown 112 | 113 | 114 | # Construct the state machine ARN by querying the region and account ID 115 | def get_state_machine_arn(name, region=default_region, sts_client=sts_client): 116 | return f"arn:aws:states:{region}:{sts_client.get_caller_identity()['Account']}:stateMachine:{name}" 117 | 118 | 119 | # Construct a unique execution name from the Streamlit session ID 120 | def get_execution_name(session_id): 121 | return f"streamlit-{session_id}-{str(uuid.uuid4())[-12:]}" 122 | 123 | 124 | def get_execution_arn( 125 | state_machine_name, execution_name, region=default_region, sts_client=sts_client 126 | ): 127 | return f"arn:aws:states:{region}:{sts_client.get_caller_identity()['Account']}:execution:{state_machine_name}:{execution_name}" 128 | 129 | 130 | def start_execution( 131 | state_machine_name, 132 | session_id, 133 | input, 134 | client=sfn_client, 135 | region=default_region, 136 | sts_client=sts_client, 137 | ): 138 | response = client.start_execution( 139 | stateMachineArn=get_state_machine_arn(state_machine_name, region, sts_client), 140 | name=get_execution_name(session_id), 141 | input=input, 142 | ) 143 | return response["executionArn"] 144 | 145 | 146 | def continue_execution(task_token, task_output, client=sfn_client): 147 | client.send_task_success( 148 | taskToken=task_token, 149 | output=json.dumps(task_output), 150 | ) 151 | 152 | 153 | def describe_execution(execution_arn, client=sfn_client): 154 | execution = client.describe_execution(executionArn=execution_arn) 155 | execution_events = [] 156 | paginator = client.get_paginator("get_execution_history") 157 | for page in paginator.paginate( 158 | executionArn=execution_arn, includeExecutionData=False 159 | ): 160 | execution_events += page["events"] 161 | return get_workflow_status_markdown(execution, execution_events) 162 | 163 | 164 | def poll_for_execution_completion(execution_arn, callback_fn=None, client=sfn_client): 165 | while True: 166 | execution = client.describe_execution(executionArn=execution_arn) 167 | 168 | if callback_fn: 169 | execution_events = [] 170 | paginator = client.get_paginator("get_execution_history") 171 | for page in paginator.paginate( 172 | executionArn=execution_arn, includeExecutionData=False 173 | ): 174 | execution_events += page["events"] 175 | callback_fn(get_workflow_status_markdown(execution, execution_events)) 176 | 177 | if execution["status"] and execution["status"] != "RUNNING": 178 | return execution 179 | time.sleep(1) 180 | 181 | 182 | def poll_for_execution_task_token_or_completion( 183 | execution_arn, callback_fn=None, client=sfn_client 184 | ): 185 | while True: 186 | # Check if execution is still running 187 | # When the execution is waiting on a task token, its status is still RUNNING. 188 | response = client.describe_execution(executionArn=execution_arn) 189 | 190 | if callback_fn: 191 | execution_events = [] 192 | paginator = client.get_paginator("get_execution_history") 193 | for page in paginator.paginate( 194 | executionArn=execution_arn, includeExecutionData=False 195 | ): 196 | execution_events += page["events"] 197 | callback_fn(get_workflow_status_markdown(response, execution_events)) 198 | 199 | if response["status"] and response["status"] != "RUNNING": 200 | return response 201 | 202 | # Check if execution is waiting on a task token 203 | response = client.get_execution_history( 204 | executionArn=execution_arn, 205 | reverseOrder=True, 206 | maxResults=1, 207 | includeExecutionData=True, 208 | ) 209 | 210 | most_recent_event = response["events"][0] 211 | if ( 212 | most_recent_event["type"] == "TaskSubmitted" 213 | and most_recent_event["taskSubmittedEventDetails"]["resource"] 214 | == "invoke.waitForTaskToken" 215 | ): 216 | output = json.loads( 217 | most_recent_event["taskSubmittedEventDetails"]["output"] 218 | ) 219 | return { 220 | "status": "PAUSED", 221 | "task_payload": output["Payload"], 222 | } 223 | 224 | time.sleep(1) 225 | 226 | 227 | def is_execution_completed(execution_arn, client=sfn_client): 228 | response = client.describe_execution(executionArn=execution_arn) 229 | return response["status"] and response["status"] != "RUNNING" 230 | --------------------------------------------------------------------------------