[Agents for Amazon Bedrock] DevelopersIO 著者の 得意分野や、どのようなペースで記事を書いているかを答えてくれるエージェントを作成してみました

2024.05.21

1 はじめに

CX事業本部製造ビジネステクノロジー部の平内(SIN)です。

Agents for Amazon Bedrockを使用すると、Lambdaで必要情報を収集して、質問に答えるエージェントを簡単に作成することができます。

今回は、DevelopersIOの著者の得意とする分野や、どれぐらいのペースで記事を書いているかなどを答えてくれるエージェントを作成してみました。

はじめに、作成したエージェントをテストしている様子です。完璧とは言えませんが、ある程度の精度は出ているように思います。

2 構成

構成は、下図のとおりで、動作は次のようになります。


① ユーザーは、「著者名」を指定して、エージェントに問いかけます
② エージェントは、アクションとして登録されているLambdaを起動します。この際「著者名」がパラメータとして渡されます
③ Lambdaは、DeveloperIOの著者ページをダウンロードし、各記事の「タイトル」、「投稿日」、「シェア数」の一覧を作成し、エージェントに返します
④ エージェントは、Lambdaから取得した記事一覧を元に、ユーザーへのレスポンスを生成し、返答します
⑤ なお、エージェントとしては、用意されたLambdaが、「著者の記事一覧取得」に利用できることは、OpenAPI.yamlの記述内容で把握しています

3 エージェント

エージェントは、以下の内容で作成されています。

  • エージェント名: agent-author-information
  • エージェントの説明: DevIOの著者を案内する
  • モデル: Anthropic Claude V2.1
  • エージェントリソースロール: 作成時には、「新しいサービスロールを作成して使用」を選択しいます
  • エージェント向け指示: あなたは、Developer IO ブログの{著者}について案内するプロです.....

ここで「エージェント向け指示」は、非常に重要です。1200文字以内という制約がありますが、エージェントの行動を、できるだけ詳細に指示した方が良いと思います。

あなたは、Developer IO ブログの{著者}について案内するプロです
指定された{著者}で、{記事一覧API}からプログの一覧を取得し、
それを要約して{案内文章}を作成してください

## 案内文書
{案内文書}は、日本語で作成してください
{案内文書}には、以下の内容を含めてください
1. {著者}が書いているブログの要約
2. {著者}が得意としている分野
3. 記事を書いている間隔
4. 最もシェア数の多い記事

{案内文書}の一例は、以下のとおりです

xxxさんは、AAAやBBBに関する記事をCCCのペースで書いています 
得意としている分野は、DDD、EEEなどです。
最近、最もシェア数の多い記事は、 「XXXXXXXX」 (yyyy.mm.dd) - XXXシェア でした。

## 記事一覧API

{記事一覧API}で、{著者}が書いているブログの一覧が取得できます
{記事一覧API}のパラメータには、{著者}が必要です
{記事一覧API}の戻り値には、複数の{ブログ情報}が含まれています

## ブログ情報
各{ブログ情報}には、title:タイトル share:シェア数 date:公開日 が含まれます
シェア数の多い記事は、人気のある記事です

モデルは、2024/05/21現在、下記から選択可能ですが、現時点では、日本語で使用するならClaudeが良いかもしれません。

また、後述しますが、エージェントに必要な権限は、「新しいサービスロールを作成して使用」を選択することで、自動的に生成されます。

4 Lambda

エージェントが必要とする「著者の記事一覧」は、Lambdaで取得しています。

DevelopersIOには、著者毎のページがあり、直近60個の記事の一覧を確認できます。 Lambdaは、著者名をパラメータとして受取り、この著者ページをダウンロードしています。

※ URLに使用される著者名は、実は、記事に表示されている著者名と一致しているわけでは無いのですが、この変換については、今回、省略しています。

https://dev.classmethod.jp/author/{著者名}/

ダウンロードした著者ページから、記事の一覧情報を取得し、下記のようなレスポンスを生成しています。

[
    {
        title: 記事1のタイトル,
        date: 投稿日,
        share: シェア数
    },
    {
        title: 記事2のタイトル,
        date: 投稿日,
        share: シェア数
    },
    ...
]

情報の抽出には、BeautifulSoupを使用させて頂きました。

BeautifulSoupを使用すると、ダウンロードしたhtmlをパースしてdomとしてアクセスできます。 著者ページの記事は、下記のタグで構成されていたので、これを抽出しました。

<div class="post-container" >
    <h3 class="post-title" >[YOLOv8] 「きのこの山」に潜伏する「たけのこの里」を機械学習で〜</h3>
    <div class="sub-content" >
        <p class="date">2024.02.19</p>
    <div class="share" >51</div>
def get_article_info(post):
    title = post.find("h3", class_="post-title").text.strip()

    sub_context = post.find("div", class_="sub-content")
    date = sub_context.find("p", class_="date").text.strip()
    share = post.find("div", class_="share").text.strip()

    return {
        "title": title,
        "date": date,
        "share": share,
    }

def get_articles(url: str) -> str:
    soup = get_html_from_url(url)
    dom = soup.find("body")

    articles = []
    for post in dom.find_all("div", class_="post-container"):
        articles.append(get_article_info(post))

    return articles
  • 著者ページ内の1記事

  • 1記事内のタイトル・投稿日・シェア数

Lambdaが呼び出される際のパラーメータ及び、レスポンスの形式は、下記に記載されています。
Configure Lambda functions to send information an Amazon Bedrock agent elicits from the user to fulfill an action groups in Amazon Bedrock

上記に従って作成されたコードは、下記のとおりです。

lambda_funcitons.py

import json
import urllib.request
import chardet
from bs4 import BeautifulSoup


def get_html_from_url(url):
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req) as res:
        body = res.read()

    chardet_result = chardet.detect(body)
    encoding = chardet_result["encoding"]

    html_doc = body.decode(encoding)
    return BeautifulSoup(html_doc, "html.parser")


def get_article_info(post):
    title = post.find("h3", class_="post-title").text.strip()

    sub_context = post.find("div", class_="sub-content")
    date = sub_context.find("p", class_="date").text.strip()
    share = post.find("div", class_="share").text.strip()

    return {
        "title": title,
        "date": date,
        "share": share,
    }


def get_articles(url: str) -> str:
    soup = get_html_from_url(url)
    dom = soup.find("body")

    articles = []
    for post in dom.find_all("div", class_="post-container"):
        articles.append(get_article_info(post))

    return articles


def lambda_handler(event, context):

    action_group = event["actionGroup"]
    http_method = event["httpMethod"]
    api_path = event["apiPath"]

    user = ""
    if api_path == "/get_articles":
        properties = event["requestBody"]["content"]["application/json"]["properties"]
        for item in properties:
            if item["name"] == "user":
                user = item["value"]

    articles = []
    if user:
        url = "https://dev.classmethod.jp/author/{}/".format(user)
        articles = get_articles(url)

    response = {
        "actionGroup": action_group,
        "apiPath": api_path,
        "httpMethod": http_method,
        "httpStatusCode": 200,
        "responseBody": {
            "application/json": {
                "body": json.dumps({"articles": articles}, ensure_ascii=False)
            }
        },
    }

    api_response = {"messageVersion": "1.0", "response": response}
    return api_response

5 OpenAPI スキーマ

エージェントから見た時に、作成したLambdaが、どのような目的で利用できるかは、OpenAPI スキーマに記載することになります。 特に、descriptionが、参照されるため、ここを適切に設定することが必要です。

Define OpenAPI schemas for your agent's action groups in Amazon Bedrock

今回は、openapi.yamlというファイルを作成し、S3に配置することで、Lambdaと関連付けています。

openapi.yaml

openapi: 3.0.0
info:
  title: 記事一覧API
  version: 1.0.0
  description: 著者の記事の一覧を取得するAPI
paths:
  /get_articles:
    post:
      summary: 著者の記事の一覧を取得する
      description: 著者は指示に基づいて決定する必要があります。 著者が不明な場合はユーザーの判断に委ねてください。
      operationId: get_articles
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user:
                  type: string
                  description: 著者
              required:
                - user
      responses:
        "200":
          description: articless response
          content:
            application/json:
              schema:
                type: object
                properties:
                  article:
                    type: string
                    description: 記事の一覧

6 アクショングループ

LambdaとOpenAPIスキーマーは、エージェント設定で、アクショングループとして設定されています。

7 パーミッション

コンソールから作成するとエージェンに必要なパーミションは、自動的に作成されます。

  • anthropic.claude-v2モデルを使用するためのパーミッション
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AmazonBedrockAgentBedrockFoundationModelPolicyProd",
            "Effect": "Allow",
            "Action": "bedrock:InvokeModel",
            "Resource": [
                "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2:1"
            ]
        }
    ]
}
  • OpenAPIスキーマを読み込むためのパーミッション
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AmazonBedrockAgentS3PolicyProd",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::agent-author-information/openapi.yaml"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:ResourceAccount": "xxxxxxxxxxxx"
                }
            }
        }
    ]
}

なお、Agentからのアクセスを許可する Lambdaのリソースベースのポリシーは、別途作成する必要があります。

% aws lambda add-permission --function-name <Function name> \
--action lambda:InvokeFunction \
--statement-id amazon-bedrock-agent \
--principal bedrock.amazonaws.com --source-arn <ARN of Agent>

8 最後に

今回は、Agents for Amazon Bedrockで、DevIO著者の情報を案内するエージェントを作成してみました。

LLMの得意作業の1つよして「要約」がありますが、必要な情報を適切に与えることで、有意義なアプリを作成出来るかもしれません。