「AWSのWhat's Newを解説くん」」をBedrockで作りました。

皆様、Bedrockを使うとこんな素敵なことができるようです！（すごい）

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Amazon BedrockのClaude 3で、先人に倣って5ch風スレッドを作れないかと試していますが、本当に凄い <a href="https://t.co/uOabApcFR6">pic.twitter.com/uOabApcFR6</a></p>&mdash; nasuuu (@nasuvit_z) <a href="https://twitter.com/nasuvit_z/status/1780409521778950176?ref_src=twsrc%5Etfw">April 17, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AWS WAFについて語る5ch風スレッドをClaude 3 Opusで作らせたら、自称サイバーセキュリティニキがなんJのノリでWAFの解説を始めた<br>しかし何でこんなに頭に入ってきやすいのか <a href="https://t.co/4uyfGDK9mt">https://t.co/4uyfGDK9mt</a> <a href="https://t.co/ouU8F4e5DX">pic.twitter.com/ouU8F4e5DX</a></p>&mdash; nasuuu (@nasuvit_z) <a href="https://twitter.com/nasuvit_z/status/1781942288115171720?ref_src=twsrc%5Etfw">April 21, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


これに触発され、[AWSのWhat's New](https://aws.amazon.com/new/)をわかりやすく解説できるアプリをStreamlitで作ってみました。

## 解説

### 処理１：What's NewのRSSを取得する

AWSのWhat's NewはRSSが公開されているので、こいつを取得します。RSSの取得は`rss-parser`を使用します。

https://www.npmjs.com/package/rss-parser

`requests`で取得したHTTPレスポンスを`rss-parser`に渡すとパースしてくれます。

```python
rss_url = "https://aws.amazon.com/about-aws/whats-new/recent/feed/"
response = requests.get(rss_url)

rss = RSSParser.parse(response.text)
items = rss.channel.items

for item in items:
    print(item.title.content) # タイトル
    print(item.pub_date.content) # pub_date
    print(item.link.content) # Link
```

### 処理２：What's Newの本文を取得する

RSSで取得したURLから、What's Newの本文を取得します。本文の取得は`BeautifulSoup4`を使用します。

https://pypi.org/project/beautifulsoup4/

What's NewのHTMLを解析したところ、`section`タグ内のテキストを取得するといい感じであることがわかりました。

```python
link = "https://*****" # What's NewのURL
response = requests.get(link) # requestsでHTMLを取得
soup = BeautifulSoup(response.text, features="html.parser")
contents = soup.section.get_text() # <section>タグ内のテキストを取得
```

### 処理３：Claude 3で会話形式の解説を生成する

試行錯誤の結果、以下のようなプロンプトとしました。

```text
AWSのWhat's newを提示しますので、注意深く読み込んでください。

<AWSのWhat's new>
{{What's new}}
</AWSのWhat's new>

タスク
　会話形式でわかりやすく解説してください。

登場人物
- user1: AWSスペシャリスト
- user2: 新入社員

ルール
- 回答は必ず日本語で行ってください。
- ハルシネーションは許されないので、What\'s newの内容からのみ回答を生成してください。
- また、回答付不要な出力は含めず、登場人物の会話のみを出力してください。
- 回答は以下のJSON形式としてください。

{
  "conversations": [
    {
      "actor": "user1",
      "message": "..."
    },
    {
      "actor": "user2",
      "message": "..."
    },
    {
      "actor": "user1",
      "message": "..."
    }
  ]
}
```

:::note
テクニック的には以下のようなものを考慮しながら考えました。

* ドキュメントを先に示す
* ルールを示す
* 例示する

結果的には適当にやってもいい感じになりましたね
:::

### 処理４：Streamlitで画面UIを作成する

サイドバーに一覧を表示して、選択したWhat's Newを右側に表示します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/df43dc58-7d90-890a-0629-283df84a6c87.png)

選択したらこんな感じ

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41574/8eae1987-ed40-1917-c3dc-d1c4aaf4b8d1.png)


<details><summary>ソースコード全体</summary>

```python
import json

import boto3
import requests
import streamlit as st
from bs4 import BeautifulSoup
from rss_parser import RSSParser

if "rss" not in st.session_state:
    rss_url = "https://aws.amazon.com/about-aws/whats-new/recent/feed/"
    response = requests.get(rss_url)

    st.session_state["rss"] = RSSParser.parse(response.text)

rss = st.session_state["rss"]
items = rss.channel.items


def f1(**kwargs):
    st.session_state["selection"] = kwargs


st.title("AWSのWhat's Newを解説くん")
st.write("←から選んでね")


with st.sidebar:

    for item in items:
        with st.container(border=True):
            st.write(item.title.content)

            kwargs = {
                "title": item.title.content,
                "pub_date": item.pub_date.content,
                "link": item.link.content,
            }

            st.button("おしえて", key=item.guid.content, on_click=f1, kwargs=kwargs)

prompt = """
AWSのWhat's newを提示しますので、注意深く読み込んでください。

<AWSのWhat's new>
{{What's new}}
</AWSのWhat's new>

タスク
　会話形式でわかりやすく解説してください。

登場人物
- user1: AWSスペシャリスト
- user2: 新入社員

ルール
- 回答は必ず日本語で行ってください。
- ハルシネーションは許されないので、What\'s newの内容からのみ回答を生成してください。
- また、回答付不要な出力は含めず、登場人物の会話のみを出力してください。
- 回答は以下のJSON形式としてください。

{
  "conversations": [
    {
      "actor": "user1",
      "message": "..."
    },
    {
      "actor": "user2",
      "message": "..."
    },
    {
      "actor": "user1",
      "message": "..."
    }
  ]
}
"""

if "selection" in st.session_state:
    kwargs = st.session_state["selection"]

    response = requests.get(kwargs["link"])
    soup = BeautifulSoup(response.text, features="html.parser")
    contents = soup.section.get_text()

    st.subheader(kwargs["title"])
    st.write(kwargs["pub_date"])

    with st.spinner("ちょっとまってね"):

        client = boto3.client("bedrock-runtime")

        model_id = "anthropic.claude-3-sonnet-20240229-v1:0"
        response = client.invoke_model(
            modelId=model_id,
            body=json.dumps(
                {
                    "anthropic_version": "bedrock-2023-05-31",
                    "max_tokens": 1024,
                    "messages": [
                        {
                            "role": "user",
                            "content": [
                                {
                                    "type": "text",
                                    "text": prompt.replace("{{What's new}}", contents),
                                }
                            ],
                        }
                    ],
                    "temperature": 0.5,
                }
            ),
        )

    try:
        response_body = json.loads(response.get("body").read())
        conversations = json.loads(response_body["content"][0]["text"])

        for c in conversations["conversations"]:
            if c["actor"] == "user1":
                with st.chat_message("ai"):
                    st.write(c["message"])

            if c["actor"] == "user2":
                with st.chat_message("human"):
                    st.write(c["message"])

    except Exception as e:
        st.write("ごめんなさい。エラーです。")
        with st.expander("Haikuの回答："):
            st.write(response_body)

    st.link_button("ブログを確認", kwargs["link"])
```

</details>



## デモ

（諸事情によりBedrockは使わず、AnthripicのAPIを使用しています）

https://huggingface.co/spaces/moritalous/aws-whats-new-2024-0427