├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEPLOY.md ├── DEPLOY_ja.md ├── LICENSE ├── README.md ├── README_ja.md ├── bin ├── cdk_test.ts └── whats-new-summary-notifier.ts ├── cdk.context.json ├── cdk.json ├── doc ├── architecture.png ├── example_en.png └── example_ja.png ├── eslint.config.mjs ├── lambda ├── notify-to-app │ ├── index.py │ └── requirements.txt └── rss-crawler │ ├── index.py │ └── requirements.txt ├── lib └── whats-new-summary-notifier-stack.ts ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # AWS CDK 7 | cdk.out/ 8 | .cdk.staging 9 | .DS_Store 10 | 11 | # Python 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # virtual env 17 | venv/ 18 | ENV/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out/ 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.json 2 | **/*.md 3 | **/*.txt 4 | **/node_modules 5 | **/dist 6 | **/*.py 7 | **/*.yml 8 | 9 | .gitignore 10 | .npmignore 11 | .prettierignore 12 | 13 | doc 14 | 15 | LICENSE -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "bracketSpacing": true, 8 | "bracketSameLine": true, 9 | "arrowParens": "always", 10 | "parser": "typescript" 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | 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 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Deployment Options 2 | This asset uses the AWS CDK context to configure the settings. 3 | 4 | You can change the settings by modifying the values under the `context` section in the [cdk.json](cdk.json) file. The details of each configuration item are as follows: 5 | 6 | ## Common Settings 7 | * `modelRegion`: The region to use Amazon Bedrock. Enter the region code of the region you want to use from among the regions where Amazon Bedrock is available. 8 | * `modelId`: The model ID of the base model to be used with Amazon Bedrock. It supports Anthropic Claude 3 and earlier versions. Refer to the documentation for the model ID of each model. 9 | 10 | ## summarizers 11 | Configure the prompt for summarizing the input to the generative AI. 12 | 13 | * `outputLanguage`: The language of the model output. 14 | * `persona`: The role (persona) to be given to the model. 15 | 16 | ## notifiers 17 | Configure the delivery settings to the application. 18 | 19 | * `destination`: The name of application to post to. Set either `slack` or `teams` according to the destination. 20 | * `summarizerName`: The name of the summarizer to use for delivery. 21 | * `webhookUrlParameterName`: The name of the AWS Systems Manager Parameter Store parameter that stores the Webhook URL. 22 | * `rssUrl`: The RSS feed URL of the website from which you want to get the latest information. Multiple URLs can be specified. 23 | * `schedule` (optional): The interval for retrieving the RSS feed in CRON format. If this parameter is not specified, the feed will be retrieved at 00 minutes every hour. In the example below, the feed will be retrieved every 15 minutes. 24 | 25 | ```json 26 | ... 27 | "schedule": { 28 | "minute": "0/15", 29 | "hour": "*", 30 | "day": "*", 31 | "month": "*", 32 | "year": "*" 33 | } 34 | ``` 35 | 36 | # Preparing the Deployment Environment (AWS Cloud9) 37 | This procedure creates a development environment on AWS with the necessary tools installed. 38 | The environment is built using AWS Cloud9. 39 | For more details on AWS Cloud9, please refer to [What is AWS Cloud9?](https://docs.aws.amazon.com/cloud9/latest/user-guide/welcome.html). 40 | 41 | 1. Open [CloudShell](https://console.aws.amazon.com/cloudshell/home). 42 | 2. Clone this repository. 43 | ```bash 44 | git clone https://github.com/aws-samples/cloud9-setup-for-prototyping 45 | ``` 46 | 3. Move to the directory 47 | ```bash 48 | cd cloud9-setup-for-prototyping 49 | ``` 50 | 4. Change volume capacities as needed for cost optimization. 51 | ```bash 52 | cat <<< $(jq '.volume_size = 20' params.json ) > params.json 53 | ``` 54 | 5. Run the script. 55 | ```bash 56 | ./bin/bootstrap 57 | ``` 58 | 6. Move to [Cloud9](https://console.aws.amazon.com/cloud9/home), and click "Open IDE ". 59 | 60 | > [!NOTE] 61 | > The AWS Cloud9 environment created in this procedure will incur pay-per-use EC2 charges based on usage time. 62 | > It is set to automatically stop after 30 minutes of inactivity, but the charges for the instance volume (Amazon EBS) will continue to accrue. 63 | > If you want to minimize charges, please delete the environment after deployment of the asset, following the instructions in [Deleting an environment in AWS Cloud9](https://docs.aws.amazon.com/cloud9/latest/user-guide/delete-environment.html). -------------------------------------------------------------------------------- /DEPLOY_ja.md: -------------------------------------------------------------------------------- 1 | # デプロイオプション 2 | 本アセットは、AWS CDK の context で設定を変更します。 3 | 4 | [cdk.json](cdk.json) の `context` 以下の値を変更することで設定します。各設定項目についての説明は下記の通りです。 5 | 6 | ## 共通設定 7 | * `modelRegion`: Amazon Bedrock を利用するリージョン。Amazon Bedrock を利用可能なリージョンの中から、利用したいリージョンのリージョンコードを入力してください。 8 | * `modelId`: Amazon Bedrock で利用する基盤モデルの model ID。Anthropic Claude 3 およびそれ以前のバージョンに対応をしています。各モデルの model ID はドキュメントを参照ください。 9 | 10 | ## summarizers 11 | 生成 AI に入力する要約用プロンプトの設定を行います。 12 | 13 | * `outputLanguage`: モデル出力の言語。 14 | * `persona`: モデルに与える役割 (ペルソナ)。 15 | 16 | ## notifiers 17 | アプリケーションへの配信設定を行います。 18 | 19 | * `destination`: 投稿先のアプリケーション名。`slack` か `teams` のいずれかを設定してください。 20 | * `summarizerName`: 配信に使用する summarizer の名前。 21 | * `webhookUrlParameterName`: Webhook URL を格納している AWS Systems Manager Parameter Store のパラメータ名。 22 | * `rssUrl`: 最新情報を取得したい Web サイトの RSS フィード URL。URL は複数指定する事が可能です。 23 | * `schedule` (オプション): CRON 形式の RSS フィード取得間隔。本パラメータの指定がない場合は、毎時 00 分にフィードを取得します。下記の例の場合は、15 分に一度フィード取得が行われます。 24 | 25 | ```json 26 | ... 27 | "schedule": { 28 | "minute": "0/15", 29 | "hour": "*", 30 | "day": "*", 31 | "month": "*", 32 | "year": "*" 33 | } 34 | ``` 35 | 36 | # 操作環境の準備 (AWS Cloud9) 37 | 本手順では、AWS 上に必要なツールがインストールされた開発環境を作成します。環境構築には、AWS Cloud9 を使用します。 38 | AWS Cloud9 についての詳細は、[AWS Cloud9 とは?](https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/welcome.html)を参照してください。 39 | 40 | 1. [CloudShell](https://console.aws.amazon.com/cloudshell/home) を開いてください。 41 | 2. 以下のコマンドでリポジトリをクローンしてください。 42 | ```bash 43 | git clone https://github.com/aws-samples/cloud9-setup-for-prototyping 44 | ``` 45 | 3. ディレクトリに移動してください。 46 | ```bash 47 | cd cloud9-setup-for-prototyping 48 | ``` 49 | 4. コスト最適化のため必要に応じてボリュームの容量を変更します。 50 | ```bash 51 | cat <<< $(jq '.volume_size = 20' params.json ) > params.json 52 | ``` 53 | 5. スクリプトを実行してください。 54 | ```bash 55 | ./bin/bootstrap 56 | ``` 57 | 1. [Cloud9](https://console.aws.amazon.com/cloud9/home) に移動し、"Open IDE" をクリックします。 58 | 59 | > [!NOTE] 60 | > 本手順で作成した AWS Cloud9 環境は、利用時間に応じて EC2 料金が従量課金で発生します。 61 | > 30 分未操作の場合は自動停止する設定になっていますが、インスタンスボリューム (Amazon EBS) の課金は継続して発生するため、 62 | > 料金発生を最小限にしたい場合は、アセットのデプロイ後に [AWS Cloud9 で環境を削除する](https://docs.aws.amazon.com/ja_jp/cloud9/latest/user-guide/delete-environment.html)に従って環境の削除を行ってください。 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Whats New Summary Notifier 2 | 3 | **[日本語はこちら](README_ja.md)** 4 | 5 | **Whats New Summary Notifier** is a sample implementation of a generative AI application that summarizes the content of AWS What's New and other web articles in multiple languages when there is an update, and delivers the summary to Slack or Microsoft Teams. 6 | 7 |

8 | example 9 |

10 | 11 | ## Architecture 12 | 13 | This stack create following architecture. 14 | 15 | ![architecture](doc/architecture.png) 16 | 17 | ## Prerequisites 18 | - An environment where you can execute Unix commands (Mac, Linux, ...) 19 | - If you don't have such an environment, you can also use AWS Cloud9. Please refer to [Preparing the Operating Environment (AWS Cloud9)](DEPLOY.md). 20 | - aws-cdk 21 | - You can install it with `npm install -g aws-cdk`. For more details, please refer to the [AWS documentation](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html). 22 | - Docker 23 | - Docker is required to build Lambda functions using the [`aws-lambda-python-alpha`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html) construct. Please refer to the [Docker documentation](https://docs.docker.com/engine/install/) for more information. 24 | 25 | ## Deployment Steps 26 | > [!IMPORTANT] 27 | > This repository is set up to use the Anthropic Claude 3 Sonnet model in the US East (N. Virginia) region (us-east-1) by default. Please open the [Model access screen (us-east-1)](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess), check the Anthropic Claude 3 Sonnet option, and click Save changes. 28 | 29 | ### Create Webhook URL 30 | Create the Webhook URL required for the notifications. 31 | 32 | #### For Microsoft Teams 33 | First open the `cdk.json` file and change the `destination` value in the `context`-`notifiers` section from `slack` to `teams`. Then, refer to [this documentation](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=newteams%2Cdotnet) to create the Webhook URL. 34 | 35 | #### For Slack 36 | Refer to [this documentation](https://slack.com/help/articles/17542172840595-Build-a-workflow--Create-a-workflow-in-Slack) to create the Webhook URL. Select "Add a Variable" and create the following 5 variables, all with the Text data type: 37 | 38 | * `rss_time`: The time the article was posted 39 | * `rss_link`: The URL of the article 40 | * `rss_title`: The title of the article 41 | * `summary`: A summary of the article 42 | * `detail`: A bulleted description of the article 43 | 44 | ### Create AWS Systems Manager Parameter Store 45 | 46 | Use Parameter Store to securely store the notification URL. 47 | 48 | #### Put into Parameter Store (AWS CLI) 49 | 50 | ``` 51 | aws ssm put-parameter \ 52 | --name "/WhatsNew/URL" \ 53 | --type "SecureString" \ 54 | --value "" 55 | ``` 56 | 57 | ### Changing the Language Setting (Optional) 58 | This asset is set up to output summaries in Japanese (日本語) by default. If you want to generate output in other languages such as English, open the `cdk.json` file and change the `summarizerName` value inside the `notifiers` object within the `context` section from `AwsSolutionsArchitectJapanese` to `AwsSolutionsArchitectEnglish` or another language. For more information on other configuration options, please refer to the [Deployment Guide](DEPLOY.md). For more information on other configuration options, please refer to the [Deployment Guide](DEPLOY.md). 59 | 60 | ### Execute the deployment 61 | **Initialize** 62 | 63 | If you haven't used CDK in this region before, run the following command: 64 | 65 | ``` 66 | cdk bootstrap 67 | ``` 68 | 69 | **Verify no errors** 70 | ``` 71 | cdk synth 72 | ``` 73 | 74 | **Execute Deployment** 75 | 76 | ``` 77 | cdk deploy 78 | ``` 79 | 80 | ## Delete Stack 81 | If no longer needed, run the following command to delete the stack: 82 | ``` 83 | cdk destroy 84 | ``` 85 | By default, some resources such as the Amazon DynamoDB table are set to not be deleted. 86 | If you need to completely delete everything, you will need to access the remaining resources and manually delete them. 87 | 88 | ## Third Party Services 89 | This code interacts with Slack or Microsoft Teams which has terms published at [Terms Page (Slack)](https://slack.com/main-services-agreement) / [Terms Page (Microsoft 365)](https://www.microsoft.com/en/servicesagreement), and pricing described at [Pricing Page (Slack)](https://slack.com/pricing) / [Pricing Page (Microsoft 365)](https://www.microsoft.com/en-us/microsoft-365/business/compare-all-microsoft-365-business-products?&activetab=tab:primaryr2). You should be familiar with the pricing and confirm that your use case complies with the terms before proceeding. -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # Whats New Summary Notifier 2 | 3 | **Whats New Summary Notifier** は、AWS 最新情報 (What's New) などのウェブ記事に更新があった際に記事内容を Amazon Bedrock で要約し、Slack や Microsoft Teams への配信を行う生成 AI アプリケーションのサンプル実装です。 4 | 5 |

6 | example 7 |

8 | 9 | ## アーキテクチャ 10 | 11 | ![architecture](doc/architecture.png) 12 | 13 | ## 前提条件 14 | - Unix コマンドを実行できる環境 (Mac、Linux、...) 15 | - そのような環境がない場合は、AWS Cloud9 を使用することも可能です。[操作環境の準備 (AWS Cloud9)](DEPLOY_ja.md) をご参照ください。 16 | - aws-cdk 17 | - `npm install -g aws-cdk` でインストール可能です。詳しくは [AWS ドキュメント](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html)を参考にしてください。 18 | - Docker 19 | - [`aws-lambda-python-alpha`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html) コンストラクトで Lambda をビルドするために Docker が必要です。詳しくは [Docker ドキュメント](https://docs.docker.com/engine/install/)を参考にしてください。 20 | 21 | ## デプロイ手順 22 | > [!IMPORTANT] 23 | > このリポジトリでは、デフォルトで米国東部 (バージニア北部) リージョン (us-east-1) の Anthropic Claude 3 Sonnet モデルを利用する設定になっています。[Model access 画面 (us-east-1)](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess)を開き、Anthropic Claude 3 Sonnet にチェックして Save changes してください。 24 | 25 | ### Webhook URL の取得 26 | 通知に必要となる Webhook URL の払い出しを行います。 27 | 28 | #### Microsoft Teams の場合 29 | 30 | まず `cdk.json` を開き、`context` の`notifiers`内、`destination` を `slack` から `teams` に書き換えてください。次に、[こちらのドキュメント](https://learn.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=newteams%2Cdotnet)を参考にして Webhook URL を取得してください。 31 | 32 | #### Slack の場合 33 | [こちらのドキュメント](https://slack.com/intl/ja-jp/help/articles/360041352714-%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B---Slack-%E5%A4%96%E9%83%A8%E3%81%A7%E9%96%8B%E5%A7%8B%E3%81%95%E3%82%8C%E3%82%8B%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)を参考にして Webhook URL を取得してください。「変数を追加する」を選び、次の 5 つの変数をすべてテキストデータタイプで作成します。 34 | 35 | * `rss_time`: 記事の投稿時間 36 | * `rss_link`: 記事の URL 37 | * `rss_title`: 記事のタイトル 38 | * `summary`: 記事の要約 39 | * `detail`: 記事の箇条書き説明 40 | 41 | ### AWS Systems Manager Parameter Store を作成 42 | 43 | Parameter Store を使って 通知用の URL をセキュアに格納します。 44 | 45 | #### パラメータストア登録 (AWS CLI) 46 | 47 | ``` 48 | aws ssm put-parameter \ 49 | --name "/WhatsNew/URL" \ 50 | --type "SecureString" \ 51 | --value "" 52 | ``` 53 | 54 | ### 言語設定の変更 (オプション) 55 | このアセットはデフォルトで日本語の要約を出力するように設定されています。英語等の他言語の出力を行う場合は、`cdk.json` を開き、`context` 内の `notifiers` 内の `summarizerName` を `AwsSolutionsArchitectJapanese` から `AwsSolutionsArchitectEnglish` などに書き換えてください。その他の設定オプションについては[デプロイガイド](DEPLOY_ja.md)を参照してください。 56 | 57 | ### デプロイの実行 58 | **初期化** 59 | 60 | このリージョンで CDK を使用したことがない場合は、次のコマンドを実行します。 61 | 62 | ``` 63 | cdk bootstrap 64 | ``` 65 | 66 | **エラーがないことを確認** 67 | ``` 68 | cdk synth 69 | ``` 70 | 71 | **デプロイの実行** 72 | 73 | ``` 74 | cdk deploy 75 | ``` 76 | 77 | ## スタックの削除 78 | 不要になった場合は以下のコマンドを実行しスタックを削除します。 79 | ``` 80 | cdk destroy 81 | ``` 82 | デフォルトでは Amazon DynamoDB テーブルなど一部のリソースが削除されず残る設定となっています。 83 | 完全な削除が必要な場合は、残存したリソースにアクセスし、手動で削除を行ってください。 84 | 85 | ## Third Party Services 86 | このコードは 3rd Party Application である Slack または Microsoft Teams と連携します。利用規約 [Terms Page (Slack)](https://slack.com/main-services-agreement) / [Terms Page (Microsoft 365)](https://www.microsoft.com/en/servicesagreement) や価格設定 [Pricing Page (Slack)](https://slack.com/pricing) / [Pricing Page (Microsoft 365)](https://www.microsoft.com/en-us/microsoft-365/business/compare-all-microsoft-365-business-products?&activetab=tab:primaryr2) はこちらに公開されています。始める前に、価格設定を確認し、使用目的が利用規約に準拠していることを確認することを推奨します。 -------------------------------------------------------------------------------- /bin/cdk_test.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { WhatsNewSummaryNotifierStack } from '../lib/whats-new-summary-notifier-stack'; 5 | import { AwsSolutionsChecks } from 'cdk-nag'; 6 | import { Aspects } from 'aws-cdk-lib'; 7 | 8 | const app = new cdk.App(); 9 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 10 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); 11 | new WhatsNewSummaryNotifierStack(app, 'WhatsNewSummaryNotifierStack', {}); 12 | -------------------------------------------------------------------------------- /bin/whats-new-summary-notifier.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { WhatsNewSummaryNotifierStack } from '../lib/whats-new-summary-notifier-stack'; 5 | 6 | const app = new cdk.App(); 7 | new WhatsNewSummaryNotifierStack(app, 'WhatsNewSummaryNotifierStack', { 8 | env: { 9 | account: process.env.CDK_DEFAULT_ACCOUNT, 10 | region: process.env.CDK_DEFAULT_REGION, 11 | }, 12 | /* If you don't specify 'env', this stack will be environment-agnostic. 13 | * Account/Region-dependent features and context lookups will not work, 14 | * but a single synthesized template can be deployed anywhere. */ 15 | /* Uncomment the next line to specialize this stack for the AWS Account 16 | * and Region that are implied by the current CLI configuration. */ 17 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 18 | /* Uncomment the next line if you know exactly what Account and Region you 19 | * want to deploy the stack to. */ 20 | // env: { account: '123456789012', region: 'us-east-1' }, 21 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 22 | }); 23 | -------------------------------------------------------------------------------- /cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "acknowledged-issue-numbers": [ 3 | 21902 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/whats-new-summary-notifier.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "modelRegion": "us-east-1", 21 | "modelId": "anthropic.claude-3-sonnet-20240229-v1:0", 22 | "summarizers": { 23 | "AwsSolutionsArchitectEnglish": { 24 | "outputLanguage": "English.", 25 | "persona": "solutions architect in AWS" 26 | }, 27 | "AwsSolutionsArchitectJapanese": { 28 | "outputLanguage": "Japanese. Each sentence must be output in polite and formal desu/masu style", 29 | "persona": "solutions architect in AWS" 30 | } 31 | }, 32 | "notifiers": { 33 | "AwsWhatsNew": { 34 | "destination": "slack", 35 | "summarizerName": "AwsSolutionsArchitectJapanese", 36 | "webhookUrlParameterName": "/WhatsNew/URL", 37 | "rssUrl": { 38 | "What’s new": "https://aws.amazon.com/about-aws/whats-new/recent/feed/" 39 | } 40 | } 41 | }, 42 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 43 | "@aws-cdk/core:stackRelativeExports": true, 44 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 45 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 46 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 47 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 48 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 49 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 50 | "@aws-cdk/core:checkSecretUsage": true, 51 | "@aws-cdk/aws-iam:minimizePolicies": true, 52 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 53 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 54 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 55 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 56 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 57 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 58 | "@aws-cdk/core:enablePartitionLiterals": true, 59 | "@aws-cdk/core:target-partitions": [ 60 | "aws", 61 | "aws-cn" 62 | ] 63 | } 64 | } -------------------------------------------------------------------------------- /doc/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/whats-new-summary-notifier/0efb8982d062ae3f8808551409e740ded5b94d81/doc/architecture.png -------------------------------------------------------------------------------- /doc/example_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/whats-new-summary-notifier/0efb8982d062ae3f8808551409e740ded5b94d81/doc/example_en.png -------------------------------------------------------------------------------- /doc/example_ja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/whats-new-summary-notifier/0efb8982d062ae3f8808551409e740ded5b94d81/doc/example_ja.png -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import prettierConfig from 'eslint-config-prettier'; 5 | 6 | export default [ 7 | { languageOptions: { globals: globals.browser } }, 8 | pluginJs.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | prettierConfig, 11 | ]; 12 | -------------------------------------------------------------------------------- /lambda/notify-to-app/index.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | import json 6 | import os 7 | import time 8 | import traceback 9 | 10 | import urllib.request 11 | 12 | from typing import Optional 13 | from botocore.config import Config 14 | from bs4 import BeautifulSoup 15 | from botocore.exceptions import ClientError 16 | import re 17 | 18 | MODEL_ID = os.environ["MODEL_ID"] 19 | MODEL_REGION = os.environ["MODEL_REGION"] 20 | NOTIFIERS = json.loads(os.environ["NOTIFIERS"]) 21 | SUMMARIZERS = json.loads(os.environ["SUMMARIZERS"]) 22 | 23 | ssm = boto3.client("ssm") 24 | 25 | 26 | def get_blog_content(url): 27 | """Retrieve the content of a blog post 28 | 29 | Args: 30 | url (str): The URL of the blog post 31 | 32 | Returns: 33 | str: The content of the blog post, or None if it cannot be retrieved. 34 | """ 35 | 36 | try: 37 | if url.lower().startswith(("http://", "https://")): 38 | # Use the `with` statement to ensure the response is properly closed 39 | with urllib.request.urlopen(url) as response: 40 | html = response.read() 41 | if response.getcode() == 200: 42 | soup = BeautifulSoup(html, "html.parser") 43 | main = soup.find("main") 44 | 45 | if main: 46 | return main.text 47 | else: 48 | return None 49 | 50 | else: 51 | print(f"Error accessing {url}, status code {response.getcode()}") 52 | return None 53 | 54 | except urllib.error.URLError as e: 55 | print(f"Error accessing {url}: {e.reason}") 56 | return None 57 | 58 | 59 | def get_bedrock_client( 60 | assumed_role: Optional[str] = None, 61 | region: Optional[str] = None, 62 | runtime: Optional[bool] = True, 63 | ): 64 | """Create a boto3 client for Amazon Bedrock, with optional configuration overrides 65 | 66 | Args: 67 | assumed_role (Optional[str]): Optional ARN of an AWS IAM role to assume for calling the Bedrock service. If not 68 | specified, the current active credentials will be used. 69 | region (Optional[str]): Optional name of the AWS Region in which the service should be called (e.g. "us-east-1"). 70 | If not specified, AWS_REGION or AWS_DEFAULT_REGION environment variable will be used. 71 | runtime (Optional[bool]): Optional choice of getting different client to perform operations with the Amazon Bedrock service. 72 | """ 73 | 74 | if region is None: 75 | target_region = os.environ.get( 76 | "AWS_REGION", os.environ.get("AWS_DEFAULT_REGION") 77 | ) 78 | else: 79 | target_region = region 80 | 81 | print(f"Create new client\n Using region: {target_region}") 82 | session_kwargs = {"region_name": target_region} 83 | client_kwargs = {**session_kwargs} 84 | 85 | profile_name = os.environ.get("AWS_PROFILE") 86 | if profile_name: 87 | print(f" Using profile: {profile_name}") 88 | session_kwargs["profile_name"] = profile_name 89 | 90 | retry_config = Config( 91 | region_name=target_region, 92 | retries={ 93 | "max_attempts": 10, 94 | "mode": "standard", 95 | }, 96 | ) 97 | session = boto3.Session(**session_kwargs) 98 | 99 | if assumed_role: 100 | print(f" Using role: {assumed_role}", end="") 101 | sts = session.client("sts") 102 | response = sts.assume_role( 103 | RoleArn=str(assumed_role), RoleSessionName="langchain-llm-1" 104 | ) 105 | print(" ... successful!") 106 | client_kwargs["aws_access_key_id"] = response["Credentials"]["AccessKeyId"] 107 | client_kwargs["aws_secret_access_key"] = response["Credentials"][ 108 | "SecretAccessKey" 109 | ] 110 | client_kwargs["aws_session_token"] = response["Credentials"]["SessionToken"] 111 | 112 | if runtime: 113 | service_name = "bedrock-runtime" 114 | else: 115 | service_name = "bedrock" 116 | 117 | bedrock_client = session.client( 118 | service_name=service_name, config=retry_config, **client_kwargs 119 | ) 120 | 121 | return bedrock_client 122 | 123 | 124 | def summarize_blog( 125 | blog_body, 126 | language, 127 | persona, 128 | ): 129 | """Summarize the content of a blog post 130 | Args: 131 | blog_body (str): The content of the blog post to be summarized 132 | language (str): The language for the summary 133 | persona (str): The persona to use for the summary 134 | 135 | Returns: 136 | str: The summarized text 137 | """ 138 | 139 | boto3_bedrock = get_bedrock_client( 140 | assumed_role=os.environ.get("BEDROCK_ASSUME_ROLE", None), 141 | region=MODEL_REGION, 142 | ) 143 | beginning_word = "" 144 | prompt_data = f""" 145 | {blog_body} 146 | You are a professional {persona}. 147 | Describe a new update in tags in bullet points to describe "What is the new feature", "Who is this update good for". description shall be output in tags and each thinking sentence must start with the bullet point "- " and end with "\n". Make final summary as per tags. Try to shorten output for easy reading. You are not allowed to utilize any information except in the input. output format shall be in accordance with tags. 148 | In {language}. 149 | The final summary must consists of 1 or 2 sentences. Output format is defined in tags. 150 | (bullet points of the input)(final summary) 151 | Follow the instruction. 152 | """ 153 | 154 | max_tokens = 4096 155 | 156 | user_message = { 157 | "role": "user", 158 | "content": [ 159 | { 160 | "type": "text", 161 | "text": prompt_data, 162 | } 163 | ], 164 | } 165 | 166 | assistant_message = { 167 | "role": "assistant", 168 | "content": [{"type": "text", "text": f"{beginning_word}"}], 169 | } 170 | 171 | messages = [user_message, assistant_message] 172 | 173 | body = json.dumps( 174 | { 175 | "anthropic_version": "bedrock-2023-05-31", 176 | "max_tokens": max_tokens, 177 | "messages": messages, 178 | "temperature": 0.5, 179 | "top_p": 1, 180 | "top_k": 250, 181 | } 182 | ) 183 | 184 | accept = "application/json" 185 | contentType = "application/json" 186 | outputText = "\n" 187 | 188 | try: 189 | response = boto3_bedrock.invoke_model( 190 | body=body, modelId=MODEL_ID, accept=accept, contentType=contentType 191 | ) 192 | response_body = json.loads(response.get("body").read().decode()) 193 | outputText = beginning_word + response_body.get("content")[0]["text"] 194 | print(outputText) 195 | # extract contant inside tag 196 | summary = re.findall(r"([\s\S]*?)", outputText)[0] 197 | detail = re.findall(r"([\s\S]*?)", outputText)[0] 198 | except ClientError as error: 199 | if error.response["Error"]["Code"] == "AccessDeniedException": 200 | print( 201 | f"\x1b[41m{error.response['Error']['Message']}\ 202 | \nTo troubeshoot this issue please refer to the following resources.\ \nhttps://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_access-denied.html\ 203 | \nhttps://docs.aws.amazon.com/bedrock/latest/userguide/security-iam.html\x1b[0m\n" 204 | ) 205 | else: 206 | raise error 207 | 208 | return summary, detail 209 | 210 | 211 | def push_notification(item_list): 212 | """Notify the arrival of articles 213 | 214 | Args: 215 | item_list (list): List of articles to be notified 216 | """ 217 | 218 | for item in item_list: 219 | 220 | notifier = NOTIFIERS[item["rss_notifier_name"]] 221 | webhook_url_parameter_name = notifier["webhookUrlParameterName"] 222 | destination = notifier["destination"] 223 | ssm_response = ssm.get_parameter(Name=webhook_url_parameter_name, WithDecryption=True) 224 | app_webhook_url = ssm_response["Parameter"]["Value"] 225 | 226 | item_url = item["rss_link"] 227 | 228 | # Get the blog context 229 | content = get_blog_content(item_url) 230 | 231 | # Summarize the blog 232 | summarizer = SUMMARIZERS[notifier["summarizerName"]] 233 | summary, detail = summarize_blog(content, language=summarizer["outputLanguage"], persona=summarizer["persona"]) 234 | 235 | # Add the summary text to notified message 236 | item["summary"] = summary 237 | item["detail"] = detail 238 | if destination == "teams": 239 | item["detail"] = item["detail"].replace("。\n", "。\r") 240 | msg = create_teams_message(item) 241 | else: # Slack 242 | msg = item 243 | 244 | encoded_msg = json.dumps(msg).encode("utf-8") 245 | print("push_msg:{}".format(item)) 246 | headers = { 247 | "Content-Type": "application/json", 248 | } 249 | req = urllib.request.Request(app_webhook_url, encoded_msg, headers) 250 | with urllib.request.urlopen(req) as res: 251 | print(res.read()) 252 | time.sleep(0.5) 253 | 254 | 255 | def get_new_entries(blog_entries): 256 | """Determine if there are new blog entries to notify on Slack by checking the eventName 257 | 258 | Args: 259 | blog_entries (list): List of blog entries registered in DynamoDB 260 | """ 261 | 262 | res_list = [] 263 | for entry in blog_entries: 264 | print(entry) 265 | if entry["eventName"] == "INSERT": 266 | new_data = { 267 | "rss_category": entry["dynamodb"]["NewImage"]["category"]["S"], 268 | "rss_time": entry["dynamodb"]["NewImage"]["pubtime"]["S"], 269 | "rss_title": entry["dynamodb"]["NewImage"]["title"]["S"], 270 | "rss_link": entry["dynamodb"]["NewImage"]["url"]["S"], 271 | "rss_notifier_name": entry["dynamodb"]["NewImage"]["notifier_name"]["S"], 272 | } 273 | print(new_data) 274 | res_list.append(new_data) 275 | else: # Do not notify for REMOVE or UPDATE events 276 | print("skip REMOVE or UPDATE event") 277 | return res_list 278 | 279 | 280 | def create_teams_message(item): 281 | message = { 282 | "type": "message", 283 | "attachments": [ 284 | { 285 | "contentType": "application/vnd.microsoft.card.adaptive", 286 | "content": { 287 | "type": "AdaptiveCard", 288 | "version": "1.3", 289 | "body": [ 290 | { 291 | "type": "ColumnSet", 292 | "columns": [ 293 | { 294 | "type": "Column", 295 | "width": "auto", 296 | "items": [ 297 | { 298 | "type": "Container", 299 | "id": "collapsedItems", 300 | "items": [ 301 | { 302 | "type": "TextBlock", 303 | "text": f'**{item["rss_title"]}**', 304 | }, 305 | { 306 | "type": "TextBlock", 307 | "wrap": True, 308 | "text": f'{item["summary"]}', 309 | }, 310 | ], 311 | }, 312 | { 313 | "type": "Container", 314 | "id": "expandedItems", 315 | "isVisible": False, 316 | "items": [ 317 | { 318 | "type": "TextBlock", 319 | "wrap": True, 320 | "text": f'{item["detail"]}', 321 | } 322 | ], 323 | }, 324 | ], 325 | } 326 | ], 327 | }, 328 | { 329 | "type": "Container", 330 | "items": [ 331 | { 332 | "type": "ColumnSet", 333 | "columns": [ 334 | { 335 | "type": "Column", 336 | "width": "stretch", 337 | "items": [ 338 | { 339 | "type": "TextBlock", 340 | "text": "see less", 341 | "id": "collapse", 342 | "isVisible": False, 343 | "wrap": True, 344 | "color": "Accent", 345 | }, 346 | { 347 | "type": "TextBlock", 348 | "text": "see more", 349 | "id": "expand", 350 | "wrap": True, 351 | "color": "Accent", 352 | }, 353 | ], 354 | } 355 | ], 356 | "selectAction": { 357 | "type": "Action.ToggleVisibility", 358 | "targetElements": [ 359 | "collapse", 360 | "expand", 361 | "expandedItems", 362 | ], 363 | }, 364 | } 365 | ], 366 | }, 367 | ], 368 | "actions": [ 369 | { 370 | "type": "Action.OpenUrl", 371 | "title": "Open Link", 372 | "wrap": True, 373 | "url": f'{item["rss_link"]}', 374 | } 375 | ], 376 | "msteams": {"width": "Full"}, 377 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 378 | }, 379 | } 380 | ], 381 | } 382 | 383 | return message 384 | 385 | 386 | def handler(event, context): 387 | """Notify about blog entries registered in DynamoDB 388 | 389 | Args: 390 | event (dict): Information about the updated items notified from DynamoDB 391 | """ 392 | 393 | try: 394 | new_data = get_new_entries(event["Records"]) 395 | if 0 < len(new_data): 396 | push_notification(new_data) 397 | except Exception as e: 398 | print(traceback.print_exc()) 399 | -------------------------------------------------------------------------------- /lambda/notify-to-app/requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4 2 | boto3 >= 1.34.64 -------------------------------------------------------------------------------- /lambda/rss-crawler/index.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import boto3 5 | import datetime 6 | import feedparser 7 | import json 8 | import os 9 | import dateutil.parser 10 | 11 | # CRAWL_BLOG_URL = json.loads(os.environ["RSS_URL"]) 12 | # NOTIFIERS = json.loads(os.environ["NOTIFIERS"]) 13 | 14 | DDB_TABLE_NAME = os.environ["DDB_TABLE_NAME"] 15 | dynamo = boto3.resource("dynamodb") 16 | table = dynamo.Table(DDB_TABLE_NAME) 17 | 18 | 19 | def recently_published(pubdate): 20 | """Check if the publication date is recent 21 | 22 | Args: 23 | pubdate (str): The publication date and time 24 | """ 25 | 26 | elapsed_time = datetime.datetime.now() - str2datetime(pubdate) 27 | print(elapsed_time) 28 | if elapsed_time.days > 7: 29 | return False 30 | 31 | return True 32 | 33 | 34 | def str2datetime(time_str): 35 | """Convert the date format from the blog text to datetime 36 | 37 | Args: 38 | time_str (str): The date and time string, e.g., "Tue, 20 Sep 2022 16:05:47 +0000" 39 | """ 40 | 41 | return dateutil.parser.parse(time_str, ignoretz=True) 42 | 43 | 44 | def write_to_table(link, title, category, pubtime, notifier_name): 45 | """Write a blog post to DynamoDB 46 | 47 | Args: 48 | link (str): The URL of the blog post 49 | title (str): The title of the blog post 50 | category (str): The category of the blog post 51 | pubtime (str): The publication date of the blog post 52 | """ 53 | try: 54 | item = { 55 | "url": link, 56 | "notifier_name": notifier_name, 57 | "title": title, 58 | "category": category, 59 | "pubtime": pubtime, 60 | } 61 | print(item) 62 | table.put_item(Item=item) 63 | except Exception as e: 64 | # Intentional error handling for duplicates to continue 65 | if e.response["Error"]["Code"] == "ConditionalCheckFailedException": 66 | print("Duplicate item put: " + title) 67 | else: 68 | # Continue for other errors 69 | print(e.message) 70 | 71 | 72 | def add_blog(rss_name, entries, notifier_name): 73 | """Add blog posts 74 | 75 | Args: 76 | rss_name (str): The category of the blog (RSS unit) 77 | entries (List): The list of blog posts 78 | """ 79 | 80 | for entry in entries: 81 | if recently_published(entry["published"]): 82 | write_to_table( 83 | entry["link"], 84 | entry["title"], 85 | rss_name, 86 | str2datetime(entry["published"]).isoformat(), 87 | notifier_name, 88 | ) 89 | else: 90 | print("Old blog entry. skip: " + entry["title"]) 91 | 92 | 93 | def handler(event, context): 94 | 95 | notifier_name, notifier = event.values() 96 | 97 | rss_urls = notifier["rssUrl"] 98 | for rss_name, rss_url in rss_urls.items(): 99 | rss_result = feedparser.parse(rss_url) 100 | print(json.dumps(rss_result)) 101 | print("RSS updated " + rss_result["feed"]["updated"]) 102 | if not recently_published(rss_result["feed"]["updated"]): 103 | # Do not process RSS feeds that have not been updated for a certain period of time. 104 | # If you want to retrieve from the past, change this number of days and re-import. 105 | print("Skip RSS " + rss_name) 106 | continue 107 | add_blog(rss_name, rss_result["entries"], notifier_name) 108 | -------------------------------------------------------------------------------- /lambda/rss-crawler/requirements.txt: -------------------------------------------------------------------------------- 1 | feedparser 2 | python-dateutil -------------------------------------------------------------------------------- /lib/whats-new-summary-notifier-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Stack, StackProps, Duration } from 'aws-cdk-lib'; 3 | import { Table, AttributeType, BillingMode, StreamViewType } from 'aws-cdk-lib/aws-dynamodb'; 4 | import { Rule, Schedule, RuleTargetInput, CronOptions } from 'aws-cdk-lib/aws-events'; 5 | import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; 6 | import { Role, Policy, ServicePrincipal, PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam'; 7 | import { Runtime, StartingPosition } from 'aws-cdk-lib/aws-lambda'; 8 | import { DynamoEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 9 | import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; 10 | import { RetentionDays } from 'aws-cdk-lib/aws-logs'; 11 | import { StringParameter } from 'aws-cdk-lib/aws-ssm'; 12 | import * as path from 'path'; 13 | 14 | export class WhatsNewSummaryNotifierStack extends Stack { 15 | constructor(scope: Construct, id: string, props?: StackProps) { 16 | super(scope, id, props); 17 | 18 | const region = Stack.of(this).region; 19 | const accountId = Stack.of(this).account; 20 | 21 | const modelRegion = this.node.tryGetContext('modelRegion'); 22 | const modelId = this.node.tryGetContext('modelId'); 23 | 24 | const notifiers: [] = this.node.tryGetContext('notifiers'); 25 | const summarizers: [] = this.node.tryGetContext('summarizers'); 26 | 27 | // Role for Lambda Function to post new entries written to DynamoDB to Slack or Microsoft Teams 28 | const notifyNewEntryRole = new Role(this, 'NotifyNewEntryRole', { 29 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'), 30 | }); 31 | notifyNewEntryRole.attachInlinePolicy( 32 | new Policy(this, 'AllowNotifyNewEntryLogging', { 33 | statements: [ 34 | new PolicyStatement({ 35 | actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], 36 | effect: Effect.ALLOW, 37 | resources: [`arn:aws:logs:${region}:${accountId}:log-group:*`], 38 | }), 39 | new PolicyStatement({ 40 | actions: ['bedrock:InvokeModel'], 41 | effect: Effect.ALLOW, 42 | resources: ['*'], 43 | }), 44 | ], 45 | }) 46 | ); 47 | 48 | // Role for Lambda function to fetch RSS and write to DynamoDB 49 | const newsCrawlerRole = new Role(this, 'NewsCrawlerRole', { 50 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'), 51 | }); 52 | newsCrawlerRole.attachInlinePolicy( 53 | new Policy(this, 'AllowNewsCrawlerLogging', { 54 | statements: [ 55 | new PolicyStatement({ 56 | actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], 57 | effect: Effect.ALLOW, 58 | resources: [`arn:aws:logs:${region}:${accountId}:log-group:*`], 59 | }), 60 | ], 61 | }) 62 | ); 63 | 64 | // DynamoDB to store RSS data 65 | const rssHistoryTable = new Table(this, 'WhatsNewRSSHistory', { 66 | partitionKey: { name: 'url', type: AttributeType.STRING }, 67 | sortKey: { name: 'notifier_name', type: AttributeType.STRING }, 68 | billingMode: BillingMode.PAY_PER_REQUEST, 69 | stream: StreamViewType.NEW_IMAGE, 70 | }); 71 | 72 | // Lambda Function to post new entries written to DynamoDB to Slack or Microsoft Teams 73 | const notifyNewEntry = new PythonFunction(this, 'NotifyNewEntry', { 74 | runtime: Runtime.PYTHON_3_11, 75 | entry: path.join(__dirname, '../lambda/notify-to-app'), 76 | handler: 'handler', 77 | index: 'index.py', 78 | timeout: Duration.seconds(180), 79 | logRetention: RetentionDays.TWO_WEEKS, 80 | role: notifyNewEntryRole, 81 | reservedConcurrentExecutions: 1, 82 | environment: { 83 | MODEL_ID: modelId, 84 | MODEL_REGION: modelRegion, 85 | NOTIFIERS: JSON.stringify(notifiers), 86 | SUMMARIZERS: JSON.stringify(summarizers), 87 | }, 88 | }); 89 | 90 | notifyNewEntry.addEventSource( 91 | new DynamoEventSource(rssHistoryTable, { 92 | startingPosition: StartingPosition.LATEST, 93 | batchSize: 1, 94 | }) 95 | ); 96 | 97 | // Allow writing to DynamoDB 98 | rssHistoryTable.grantWriteData(newsCrawlerRole); 99 | 100 | // Lambda Function to fetch RSS and write to DynamoDB 101 | const newsCrawler = new PythonFunction(this, `newsCrawler`, { 102 | runtime: Runtime.PYTHON_3_11, 103 | entry: path.join(__dirname, '../lambda/rss-crawler'), 104 | handler: 'handler', 105 | index: 'index.py', 106 | timeout: Duration.seconds(60), 107 | logRetention: RetentionDays.TWO_WEEKS, 108 | role: newsCrawlerRole, 109 | environment: { 110 | DDB_TABLE_NAME: rssHistoryTable.tableName, 111 | NOTIFIERS: JSON.stringify(notifiers), 112 | }, 113 | }); 114 | 115 | for (const notifierName in notifiers) { 116 | const notifier = notifiers[notifierName]; 117 | // const cron is a cronOption defined in a notifier. if it is not defined, set default schedule (every hour) 118 | const schedule: CronOptions = notifier['schedule'] || { 119 | minute: '0', 120 | hour: '*', 121 | day: '*', 122 | month: '*', 123 | year: '*', 124 | }; 125 | const webhookUrlParameterName = notifier['webhookUrlParameterName']; 126 | const webhookUrlParameterStore = StringParameter.fromSecureStringParameterAttributes( 127 | this, 128 | `webhookUrlParameterStore-${notifierName}`, 129 | { 130 | parameterName: webhookUrlParameterName, 131 | } 132 | ); 133 | 134 | // add permission to Lambda Role 135 | webhookUrlParameterStore.grantRead(notifyNewEntryRole); 136 | 137 | // Scheduled Rule for RSS Crawler 138 | // Run every hour, 24 hours a day 139 | // see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions 140 | const rule = new Rule(this, `CheckUpdate-${notifierName}`, { 141 | schedule: Schedule.cron(schedule), 142 | enabled: true, 143 | }); 144 | 145 | rule.addTarget( 146 | new LambdaFunction(newsCrawler, { 147 | event: RuleTargetInput.fromObject({ notifierName, notifier }), 148 | retryAttempts: 2, 149 | }) 150 | ); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whats-new-summary-notifier", 3 | "version": "0.1.0", 4 | "bin": { 5 | "whats-new-summary-notifier": "bin/whats-new-summary-notifier.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk" 12 | }, 13 | "devDependencies": { 14 | "@eslint/js": "^9.2.0", 15 | "@types/node": "20.12.11", 16 | "aws-cdk": "^2.141.0", 17 | "eslint": "^8.56.0", 18 | "eslint-config-prettier": "^9.1.0", 19 | "globals": "^15.2.0", 20 | "jest": "^27.5.1", 21 | "prettier": "3.2.5", 22 | "ts-node": "^10.9.1", 23 | "typescript": "~5.4.5", 24 | "typescript-eslint": "^7.8.0" 25 | }, 26 | "dependencies": { 27 | "@aws-cdk/aws-lambda-python-alpha": "^2.141.0-alpha.0", 28 | "aws-cdk-lib": "^2.141.0", 29 | "cdk-nag": "^2.28.113", 30 | "constructs": "^10.0.0", 31 | "source-map-support": "^0.5.21" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------