3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon Bedrockハンズオン(AWS SDK/LangChain/Lambda/Anthropic Claude2.1/3)

Last updated at Posted at 2023-12-10

最近注目を集めているの生成系AIのAWSサービス、Bedrockを使って基本的なことを試してみました。
普段はソリューションアーキテクトのようなことをしていますが、実際に作業をする機会が少なく基本的なところで結構はまりました。
そういったところを含めて、ハンズオン形式でまとめました。
AWSでの開発のしやすさを考慮して、Cloud9ベースとしています。

お役に立てれば幸いです。

(2024/04/11追記)
Cloud9(AmazonLinux2023)+LangChain最新版+Claude3(sonnet)で動作確認し、適宜コメントを追記しましたが、ごちゃごちゃとなってしまいました。

目次

Cloud9の準備

特に指定はありませんが、インスタンスタイプはt3.small、プラットフォームはAmazon Linux2で作成しました。
そのうち、Amazon Linux 2023に対応すると思いますが、現時点(2023/12/10)ではまだ使えませんでした。
古いCloud9を使っている場合は、AWS CLI/AWS SAM/boto3などのアップデートが必要となるため、可能であれば新規作成することをお勧めします。

現在では、Amazon Linux 2023に対応しているため、追記しました。

Amazon Linux2では、Python3.7ベースのため、実行時に以下の警告が出ます。

/home/ec2-user/.local/lib/python3.7/site-packages/boto3/compat.py:82: PythonDeprecationWarning: Boto3 will no longer support Python 3.7 starting December 13, 2023. To continue receiving service updates, bug fixes, and security updates please upgrade to Python 3.8 or later. More information can be found here: https://aws.amazon.com/blogs/developer/python-support-policy-updates-for-aws-sdks-and-tools/
warnings.warn(warning, PythonDeprecationWarning)

Python3.8のインストールはamazon-linux-extrasを使いました。

sudo amazon-linux-extras install python3.8

aliasがPython3.7となっているため、~/.bashrcの末尾に以下を追記し、ターミナルを再度立ち上げます。

~/.bashrc
alias python="python3.8"
alias pip="pip3.8"

(2024/04/11追記)
Amazon Linux 2023では、Python3.9がインストールされているようです。
~/.bashrcの末尾に以下を追記しました。

~/.bashrc
alias python="python3"
alias pip="pip3"

https://docs.aws.amazon.com/ja_jp/linux/al2023/ug/python.html

Amazon Bedrockの準備

Anthropic Claude2.1が使いたかったので、バージニア北部(us-east-1)を使いました。
[Amazon Bedrock] → [Model access] → [Manage model access]から、Anthropicの[Use case details submitted]を申請します。必要事項を記入するとすぐに承認されました。
なお、一回申請すると新しいモデルや別のリージョンで再申請する必要がありません。

PlaygroundsのTextやChatで簡単に試すこともできます。

Amazon Bedrockにおけるデータ保護

Amazon Bedrock は、AWSユーザーのプロンプトや継続情報をモデルのトレーニングや第三者への配布に使用することはありません。トレーニングデータは Amazon Titan の基本モデルのトレーニングに使用されたり、第三者に配布されたりすることはありません。使用タイムスタンプ、記録されたアカウント ID、サービスによって記録されたその他の情報など、その他の使用データもモデルのトレーニングには使用されません。
https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/data-protection.html

Pythonからの利用(AWS SDK)

まずは、基本となるAWS SDKを使いました。言語はPythonを使っています。
Pythonで使うAWS SDKパッケージであるboto3のバージョンは、1.33.11を使いました。

boto3のバージョンを確認し、古い場合はアップデートしてください。

$ pip show boto3
Name: boto3
Version: 1.33.11
Summary: The AWS SDK for Python
Home-page: https://github.com/boto/boto3
Author: Amazon Web Services
Author-email: 
License: Apache License 2.0
Location: /home/ec2-user/.local/lib/python3.8/site-packages
Requires: botocore, jmespath, s3transfer
Required-by: 
$ pip install -U boto3

(2024/04/11追記)
AL2023にはboto3がインストールされていなかったため、新規インストールしました。

$ pip show boto3
WARNING: Package(s) not found: boto3
$ pip install boto3
Defaulting to user installation because normal site-packages is not writeable
Collecting boto3
  Downloading boto3-1.34.80-py3-none-any.whl (139 kB)
     |████████████████████████████████| 139 kB 5.8 MB/s            
Collecting botocore<1.35.0,>=1.34.80
  Downloading botocore-1.34.80-py3-none-any.whl (12.1 MB)
     |████████████████████████████████| 12.1 MB 59.2 MB/s            
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /usr/lib/python3.9/site-packages (from boto3) (0.10.0)
Collecting s3transfer<0.11.0,>=0.10.0
  Downloading s3transfer-0.10.1-py3-none-any.whl (82 kB)
     |████████████████████████████████| 82 kB 688 kB/s             
Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /usr/lib/python3.9/site-packages (from botocore<1.35.0,>=1.34.80->boto3) (2.8.1)
Requirement already satisfied: urllib3<1.27,>=1.25.4 in /usr/lib/python3.9/site-packages (from botocore<1.35.0,>=1.34.80->boto3) (1.25.10)
Requirement already satisfied: six>=1.5 in /usr/lib/python3.9/site-packages (from python-dateutil<3.0.0,>=2.1->botocore<1.35.0,>=1.34.80->boto3) (1.15.0)
Installing collected packages: botocore, s3transfer, boto3
Successfully installed boto3-1.34.80 botocore-1.34.80 s3transfer-0.10.1
$ pip show boto3
Name: boto3
Version: 1.34.80
Summary: The AWS SDK for Python
Home-page: https://github.com/boto/boto3
Author: Amazon Web Services
Author-email: 
License: Apache License 2.0
Location: /home/ec2-user/.local/lib/python3.9/site-packages
Requires: botocore, jmespath, s3transfer
Required-by: 

PythonからAmazon Bedrockを利用する場合は、bedrock-runtimeを使用します。
bedrockもありますが、管理系のAPIのようです。

import boto3

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

Cloud9では、初期設定ではAWSプロファイルやリージョンが設定されていません。
Clientのパラメータは、以下のリファレンスに記載があり、region_nameを使います。

Amazon Bedrockの基盤モデルの呼び出しは、invoke_modelを使います。
以下のリファレンスに使い方が記載されています。
必須項目は、bodyとmodelIdのようです。

Request Syntax
response = client.invoke_model(
    body=b'bytes'|file,
    contentType='string',
    accept='string',
    modelId='string'
)

bodyには、以下のJSON形式で指定します。
promptとmax_tokens_to_sampleが必須のようです。

{
    "prompt": "\n\nHuman:<prompt>\n\nAssistant:",
    "temperature": float,
    "top_p": float,
    "top_k": int,
    "max_tokens_to_sample": int,
    "stop_sequences": ["\n\nHuman:"]
}

利用可能なModel IDを調べます。
今回は、Anthropicのclaude2.1("anthropic.claude-v2:1")を使います。

$ aws bedrock list-foundation-models --region us-east-1 |
> jq '.modelSummaries[] | select(.providerName == "Anthropic") | .modelId'
"anthropic.claude-instant-v1:2:100k"
"anthropic.claude-instant-v1"
"anthropic.claude-v1:3:18k"
"anthropic.claude-v1:3:100k"
"anthropic.claude-v1"
"anthropic.claude-v2:0:18k"
"anthropic.claude-v2:0:100k"
"anthropic.claude-v2:1:18k"
"anthropic.claude-v2:1:200k"
"anthropic.claude-v2:1"
"anthropic.claude-v2"

(2024/04/11追記)
Claude3(sonnet/haiku)が追加されていました。Opusはまだのようです。
使用する場合は、Amazon Bedrockのモデルアクセスから有効化が必要です。

$ aws bedrock list-foundation-models --region us-east-1 | jq '.modelSummaries[] | select(.providerName == "Anthropic") | .modelId'
"anthropic.claude-instant-v1:2:100k"
"anthropic.claude-instant-v1"
"anthropic.claude-v2:0:18k"
"anthropic.claude-v2:0:100k"
"anthropic.claude-v2:1:18k"
"anthropic.claude-v2:1:200k"
"anthropic.claude-v2:1"
"anthropic.claude-v2"
"anthropic.claude-3-sonnet-20240229-v1:0:28k"
"anthropic.claude-3-sonnet-20240229-v1:0:200k"
"anthropic.claude-3-sonnet-20240229-v1:0"
"anthropic.claude-3-haiku-20240307-v1:0:48k"
"anthropic.claude-3-haiku-20240307-v1:0:200k"
"anthropic.claude-3-haiku-20240307-v1:0"

上記の整理した結果をもとにPythonコードを作成します。

import boto3
import json

bedrock = boto3.client(
    service_name='bedrock-runtime',
    region_name='us-east-1'
)

response = bedrock.invoke_model(
    modelId="anthropic.claude-v2:1",
    body=json.dumps({
        "prompt": "\n\nHuman:東京の首都は?\n\nAssistant:",
        "max_tokens_to_sample" : 300
    })
)

print(response)

実行結果が以下の通りです。
bodyに回答が含まれていますが、以下の通りbotocore.response.StreamingBodyオブジェクトになっています。

'body': <botocore.response.StreamingBody object at 0x7fc90462c5b0>

$ python test-bedrock.py
{'ResponseMetadata': {'RequestId': '389aadad-ab81-4fca-8b34-34b92af67800', 'HTTPStatusCode': 200, 'HTTPHeaders': {'date': 'Sat, 09 Dec 2023 19:54:37 GMT', 'content-type': 'application/json', 'content-length': '101', 'connection': 'keep-alive', 'x-amzn-requestid': '389aadad-ab81-4fca-8b34-34b92af67800', 'x-amzn-bedrock-invocation-latency': '595', 'x-amzn-bedrock-output-token-count': '16', 'x-amzn-bedrock-input-token-count': '18'}, 'RetryAttempts': 0}, 'contentType': 'application/json', 'body': <botocore.response.StreamingBody object at 0x7fc90462c5b0>}

botocore.response.StreamingBodyオブジェクトは、readlinesメソッドで読み取れるようなので以下のように修正しました。

import boto3
import json

bedrock = boto3.client(
    service_name='bedrock-runtime',
    region_name='us-east-1'
)

response = bedrock.invoke_model(
    modelId="anthropic.claude-v2:1",
    body=json.dumps({
        "prompt": "\n\nHuman:東京の首都は?\n\nAssistant:",
        "max_tokens_to_sample" : 300
    })
)

print(response["body"].readlines())
$ python test-bedrock.py
[b'{"completion":" \xe6\x9d\xb1\xe4\xba\xac\xe3\x81\xaf\xe6\x97\xa5\xe6\x9c\xac\xe3\x81\xae\xe9\xa6\x96\xe9\x83\xbd\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82","stop_reason":"stop_sequence","stop":"\\n\\nHuman:"}']

エンコードされているため、UTF-8ででコードします。

import boto3
import json

bedrock = boto3.client(
    service_name='bedrock-runtime',
    region_name='us-east-1'
)

response = bedrock.invoke_model(
    modelId="anthropic.claude-v2:1",
    body=json.dumps({
        "prompt": "\n\nHuman:東京の首都は?\n\nAssistant:",
        "max_tokens_to_sample" : 300
    })
)

print(json.loads(response["body"].readlines()[0].decode('utf-8'))['completion'])

以下のように回答が日本語で取得できました。
promptの部分を変えることで、好きな質問ができます。

$ python test-bedrock.py
 東京は日本の首都です

(2024/04/11追記)
単純にmodelId="anthropic.claude-3-sonnet-20240229-v1:0"と書き換えただけでは以下のエラーが発生しました。

$ python test-bedrock.py 
Traceback (most recent call last):
  File "/home/ec2-user/environment/test-bedrock.py", line 9, in <module>
    response = bedrock.invoke_model(
  File "/home/ec2-user/.local/lib/python3.9/site-packages/botocore/client.py", line 565, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/ec2-user/.local/lib/python3.9/site-packages/botocore/client.py", line 1021, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.ValidationException: An error occurred (ValidationException) when calling the InvokeModel operation: "claude-3-sonnet-20240229" is not supported on this API. Please use the Messages API instead.

詳細は割愛しますが、Messages APIにしないといけないようで、以下の公式ドキュメントのサンプルを参考に修正しました。

https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html#api-inference-examples-claude-messages-code-examples

import boto3
import json

bedrock = boto3.client(
    service_name='bedrock-runtime',
    region_name='us-east-1'
)

response = bedrock.invoke_model(
    modelId="anthropic.claude-3-sonnet-20240229-v1:0",
    body=json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 1024,
        "messages": [
            {"role": "user", "content": "東京の首都は?"}
        ]
    })
)

print(json.loads(response["body"].readlines()[0].decode('utf-8'))['content'][0]['text'])
$ python test-bedrock.py 
東京は日本の首都です。

東京都は日本の首都所在地であり、東京都区部(旧東京市の区域)が政治の中心地となっています。憲法上は東京が「首都」と規定されているわけではありませんが、事実上の首都として機能しています。

政府の中枢である国会、内閣、大手企業の本社、報道機関などが東京に集中しており、日本の政治・経済・文化の中心地としての役割を果たし

Pythonからの利用(LangChain)

次に、LangChainを使います。
LangChainは、様々なLLMに対応しており、Amazon Bedrockにも対応しています。

LangChainを利用するためには、以下のコマンドでインストールが必要です。

$ pip install langchain

公式ドキュメントには、AWSプロファイルを指定するcredentials_profile_nameの例が示されていますが、
他にも、boto3のclientを指定したり、region_nameを指定することもできます。
今回は、Cloud9の一時クレデンシャル(AMTC)を利用しているため、region_nameのみを指定します。
boto3に比べて、かなりシンプルに書けました。

from langchain.llms import Bedrock

prompt = '''
Human: 日本の首都は?
Assistant:
'''

llm = Bedrock(
    region_name='us-east-1',
    model_id='anthropic.claude-v2:1'
)

print(llm(prompt))

以下が実行結果です。boto3の時よりも詳しく回答されています。

$ python test-langchain.py
 東京です。

東京は日本の首都で、人口も一番多い街です。政治の中心であり、経済やビジネスの中心地でもあります。

ファイル名をlangchain.pyにしたり、実行ディレクトリと同じ階層にあると以下のエラーが出ます。
初歩的な話ですが、ついやってしまいました。注意しましょう。

 $ python langchain.py 
Traceback (most recent call last):
  File "langchain.py", line 1, in <module>
    from langchain.llms import Bedrock
  File "/home/ec2-user/environment/bedrock/langchain.py", line 1, in <module>
    from langchain.llms import Bedrock
ModuleNotFoundError: No module named 'langchain.llms'; 'langchain' is not a package

(2024/04/11追記)

単純にClaude3に置き換えると以下のエラーが発生しました。

/home/ec2-user/.local/lib/python3.9/site-packages/langchain/llms/__init__.py:548: LangChainDeprecationWarning: Importing LLMs from langchain is deprecated. Importing from langchain will no longer be supported as of langchain==0.2.0. Please import from langchain-community instead:

`from langchain_community.llms import Bedrock`.

To install langchain-community run `pip install -U langchain-community`.
  warnings.warn(
Traceback (most recent call last):
  File "/home/ec2-user/environment/old_test_langchain.py", line 8, in <module>
    llm = Bedrock(
  File "/home/ec2-user/.local/lib/python3.9/site-packages/langchain_core/load/serializable.py", line 120, in __init__
    super().__init__(**kwargs)
  File "/home/ec2-user/.local/lib/python3.9/site-packages/pydantic/v1/main.py", line 341, in __init__
    raise validation_error
pydantic.v1.error_wrappers.ValidationError: 1 validation error for Bedrock
__root__
  Claude v3 models are not supported by this LLM.Please use `from langchain_community.chat_models import BedrockChat` instead. (type=value_error)

Claude3ではBedrockChatを使うようにとのことなので、以下のドキュメントを参考に修正しました。
https://python.langchain.com/docs/integrations/chat/bedrock/

from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage

chat = BedrockChat(
    region_name='us-east-1',
    model_id='anthropic.claude-3-sonnet-20240229-v1:0',
    model_kwargs={"temperature": 0.0}
)

messages = [
    HumanMessage(
        content="日本の首都は?"
    )
]

response = chat(messages)

print(response.content)

以下のように実行できましたが、chatの呼び出しにはinvokeを使うようにとの警告が出ています。

$ python test-langchain.py 
/home/ec2-user/.local/lib/python3.9/site-packages/langchain_core/_api/deprecation.py:117: LangChainDeprecationWarning: The function `__call__` was deprecated in LangChain 0.1.7 and will be removed in 0.2.0. Use invoke instead.
  warn_deprecated(
東京が日本の首都です。

東京は、日本の本州島の東部、関東地方に位置する巨大な都市です。人口は約3,700万人と世界有数の大都市圏を形成しています。

江戸時代には江戸と呼ばれ、徳川幕府の城下町として栄えました。明治維新後の1868年に東京と改称され、日本の政治・経済・文化の中心地となりました。

東京には皇居、国会議事堂、多くの博物館や美術館、伝統的な寺社仏閣に加え、最新のビル群やショッピング街なども集中しています。首都機能と共に、日本を代表する国際都市でもあります。

invokeを使って呼び出すことで警告は消えました。

from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage

chat = BedrockChat(
    region_name='us-east-1',
    model_id='anthropic.claude-3-sonnet-20240229-v1:0',
    model_kwargs={"temperature": 0.0}
)

messages = [
    HumanMessage(
        content="日本の首都は?"
    )
]

response = chat.invoke(messages)

print(response.content)

次に、PromptTemplateモジュールを使います。
PromptTemplateは、テンプレート文を作成し、実行時に変数を置き換えることができます。

from langchain.llms import Bedrock
from langchain.prompts import PromptTemplate

prompt = '''
Human: {country}の首都は?
Assistant:
'''

llm = Bedrock(
    region_name="us-east-1",
    model_id="anthropic.claude-v2:1"
)

print(llm(prompt.format(country="フランス")))

実行結果が以下の通りです。
country="フランス"の部分を置き換えることで、他の国を問い合わせできます。

$ python test-template.py
 パリです。

/home/ec2-user/.local/lib/python3.8/site-packages/langchain/init.py:34: UserWarning: Importing PromptTemplate from langchain root module is no longer supported. Please use langchain.prompts.PromptTemplate instead.
warnings.warn(

古い記事では、「from langchain import PromptTemplate」となっていることがあります。
注意されている通り、「from langchain.prompts import PromptTemplate」に書き換えることで警告が消えます。

(2024/04/11追記)

Templateも以下のように見直すことで動作しました。

from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate

chat = BedrockChat(
    region_name='us-east-1',
    model_id='anthropic.claude-3-sonnet-20240229-v1:0',
    model_kwargs={"temperature": 0.0}
)

template = ChatPromptTemplate.from_messages([
    ("human", "{country}の首都は?")
])

response = chat.invoke(template.invoke({"country": "フランス"}))

print(response.content)

サンプル:C++のコードをJavaに変換

ここまでくると、以下のAWSサンプルが理解できるようになります。
内容は、C++のコードをJavaに書き換えるものとなっています。

サンプルの中では、独自の関数が利用されているため、シンプルに書き替えました。
C++のコードの部分をファイルから読み取るようにしたり、指定言語を変換することで実用的になりそうです。

from langchain.llms import Bedrock
from langchain.prompts import PromptTemplate

inference_modifier = {'max_tokens_to_sample':4096, 
                      "temperature":0.5,
                      "top_k":250,
                      "top_p":1,
                      "stop_sequences": ["\n\nHuman"]
                     }

llm = Bedrock(
    region_name="us-east-1",
    model_id="anthropic.claude-v2:1",
    model_kwargs = inference_modifier
)

# Vehicle Fleet Management Code written in C++
sample_code = """
#include <iostream>
#include <string>
#include <vector>

class Vehicle {
protected:
    std::string registrationNumber;
    int milesTraveled;
    int lastMaintenanceMile;

public:
    Vehicle(std::string regNum) : registrationNumber(regNum), milesTraveled(0), lastMaintenanceMile(0) {}

    virtual void addMiles(int miles) {
        milesTraveled += miles;
    }

    virtual void performMaintenance() {
        lastMaintenanceMile = milesTraveled;
        std::cout << "Maintenance performed for vehicle: " << registrationNumber << std::endl;
    }

    virtual void checkMaintenanceDue() {
        if ((milesTraveled - lastMaintenanceMile) > 10000) {
            std::cout << "Vehicle: " << registrationNumber << " needs maintenance!" << std::endl;
        } else {
            std::cout << "No maintenance required for vehicle: " << registrationNumber << std::endl;
        }
    }

    virtual void displayDetails() = 0;

    ~Vehicle() {
        std::cout << "Destructor for Vehicle" << std::endl;
    }
};

class Truck : public Vehicle {
    int capacityInTons;

public:
    Truck(std::string regNum, int capacity) : Vehicle(regNum), capacityInTons(capacity) {}

    void displayDetails() override {
        std::cout << "Truck with Registration Number: " << registrationNumber << ", Capacity: " << capacityInTons << " tons." << std::endl;
    }
};

class Car : public Vehicle {
    std::string model;

public:
    Car(std::string regNum, std::string carModel) : Vehicle(regNum), model(carModel) {}

    void displayDetails() override {
        std::cout << "Car with Registration Number: " << registrationNumber << ", Model: " << model << "." << std::endl;
    }
};

int main() {
    std::vector<Vehicle*> fleet;

    fleet.push_back(new Truck("XYZ1234", 20));
    fleet.push_back(new Car("ABC9876", "Sedan"));

    for (auto vehicle : fleet) {
        vehicle->displayDetails();
        vehicle->addMiles(10500);
        vehicle->checkMaintenanceDue();
        vehicle->performMaintenance();
        vehicle->checkMaintenanceDue();
    }

    for (auto vehicle : fleet) {
        delete vehicle; 
    }

    return 0;
}
"""

# Create a prompt template that has multiple input variables
multi_var_prompt = PromptTemplate(
    input_variables=["code", "srcProgrammingLanguage", "targetProgrammingLanguage"], 
    template="""

Human: You will be acting as an expert software developer in {srcProgrammingLanguage} and {targetProgrammingLanguage}. 
You will tranlslate below code from {srcProgrammingLanguage} to {targetProgrammingLanguage} while following coding best practices.
<code>
{code}
</code>

Assistant: """
)

# Pass in values to the input variables
prompt = multi_var_prompt.format(code=sample_code, srcProgrammingLanguage="C++", targetProgrammingLanguage="Java")

print(llm(prompt))

生成されたJavaコードが以下の通りです。
検証していませんが、ぱっと見ちゃんと書けていそうですね。

```java
import java.util.ArrayList;

class Vehicle {
    protected String registrationNumber;
    protected int milesTraveled;
    protected int lastMaintenanceMile;

    public Vehicle(String regNum) {
        this.registrationNumber = regNum;
        this.milesTraveled = 0; 
        this.lastMaintenanceMile = 0;
    }

    public void addMiles(int miles) {
        this.milesTraveled += miles;
    }

    public void performMaintenance() {
        this.lastMaintenanceMile = this.milesTraveled;
        System.out.println("Maintenance performed for vehicle: " + this.registrationNumber);
    }

    public void checkMaintenanceDue() {
        if ((this.milesTraveled - this.lastMaintenanceMile) > 10000) {
            System.out.println("Vehicle: " + this.registrationNumber + " needs maintenance!");
        } else {
            System.out.println("No maintenance required for vehicle: " + this.registrationNumber);
        }
    }
    
    public void displayDetails() {
    }
}

class Truck extends Vehicle {
    private int capacityInTons;

    public Truck(String regNum, int capacity) {
        super(regNum);
        this.capacityInTons = capacity;
    }

    public void displayDetails() {
        System.out.println("Truck with Registration Number: " + this.registrationNumber + ", Capacity: " + this.capacityInTons + " tons.");
    }
}

class Car extends Vehicle {
    private String model;

    public Car(String regNum, String carModel) {
        super(regNum);
        this.model = carModel;
    }

    public void displayDetails() {
        System.out.println("Car with Registration Number: " + this.registrationNumber + ", Model: " + this.model + ".");
    }
}

public class Main {
    public static void main(String[] args) {
        ArrayList<Vehicle> fleet = new ArrayList<Vehicle>();

        fleet.add(new Truck("XYZ1234", 20)); 
        fleet.add(new Car("ABC9876", "Sedan"));

        for(Vehicle vehicle : fleet) {
            vehicle.displayDetails();
            vehicle.addMiles(10500);
            vehicle.checkMaintenanceDue();
            vehicle.performMaintenance();
            vehicle.checkMaintenanceDue();
        }
    }
}
```

The key differences from C++ are:

- Uses `import` instead of `include` 
- No destructors or delete required, garbage collection handles it
- Use `ArrayList` instead of `vector`
- Inheritance with `extends` instead of `:` 
- Access modifiers like `private` 
- `Override` annotation for overridden methods
- Other syntax differences like semicolons, getter/setters

I followed Java best practices like access modifiers, camelCase, and object-oriented design principles. Let me know if you have any other questions!

API Gateway + LambdaからBedrockを利用

次に、Bedrockを使用したLambdaをAPI Gatewayから呼び出します。
今回作成したコードは、以下のGitHubにもコミットしています。

まずは、sam initを使って、ひな形を作成しました。

$ sam init

You can preselect a particular runtime or package type when using the `sam init` experience.
Call `sam init --help` to learn more.

Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Data processing
        3 - Hello World Example with Powertools for AWS Lambda
        4 - Multi-step workflow
        5 - Scheduled task
        6 - Standalone function
        7 - Serverless API
        8 - Infrastructure event management
        9 - Lambda Response Streaming
        10 - Serverless Connector Hello World Example
        11 - Multi-step workflow with Connectors
        12 - Full Stack
        13 - Lambda EFS example
        14 - DynamoDB Example
        15 - Machine Learning
Template: 1

Use the most popular runtime and package type? (Python and zip) [y/N]: y

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: 

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: 

Project name [sam-app]: sam-bedrock-app

    -----------------------
    Generating application:
    -----------------------
    Name: sam-bedrock-app
    Runtime: python3.9
    Architectures: x86_64
    Dependency Manager: pip
    Application Template: hello-world
    Output Directory: .
    Configuration file: sam-bedrock-app/samconfig.toml
    
    Next steps can be found in the README file at sam-bedrock-app/README.md
        

Commands you can use next
=========================
[*] Create pipeline: cd sam-bedrock-app && sam pipeline init --bootstrap
[*] Validate SAM template: cd sam-bedrock-app && sam validate
[*] Test Function in the Cloud: cd sam-bedrock-app && sam sync --stack-name {stack-name} --watch

template.yamlを以下のように書き換えました。
Hello WorldをBedrock Appに書き換え、Timeoutを60秒にしました。
また、LambdaにBedrockが利用可能なポリシーを付与しています。

API Gatewayのリソースポリシーを使って送信元を制限しています。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-bedrock-app

  Sample SAM Template for sam-bedrock-app

Parameters:
  SourceIp:
    Type: String

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 60
    MemorySize: 128
  Api:
    Auth:
      ResourcePolicy:
        CustomStatements:
        - Effect: Allow
          Principal: "*"
          Action: execute-api:Invoke
          Resource: "*"
          Condition:
            IpAddress:
              aws:SourceIp: !Sub ${SourceIp}

Resources:
  BedrockAppFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: bedrock_app/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Policies:
        - Statement:
            - Effect: Allow
              Action: 'bedrock:*'
              Resource: '*'
      Events:
        BedrockApp:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /bedrock_app
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  BedrockAppApi:
    Description: "API Gateway endpoint URL for Prod stage for Bedrock App function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/bedrock_app/"
  BedrockAppFunction:
    Description: "Bedrock App Lambda Function ARN"
    Value: !GetAtt BedrockAppFunction.Arn
  BedrockAppFunctionIamRole:
    Description: "Implicit IAM Role created for BedrockApp function"
    Value: !GetAtt BedrockAppFunctionRole.Arn

app.pyを以下のように書き換えました。
上記で作成したサンプルをほぼそのまま流用しています。
ひな形がGETだったため、クエリストリングで受け取るようにしていますが、
実際はPOSTにして、クエリパラメータにした方がよいと思います。

sam-bedrock-app/bedrock_app/app.py
import json
from langchain.llms import Bedrock
from langchain.prompts import PromptTemplate

def lambda_handler(event, context):
    print(json.dumps(event))
    
    prompt = '''
    Human: {country}の首都は?
    Assistant:
    '''
    
    country = event["queryStringParameters"]["country"]
    
    llm = Bedrock(
        region_name="us-east-1",
        model_id="anthropic.claude-v2:1"
    )
    
    answer = llm(prompt.format(country=country))
    
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json;charset=UTF-8",
        },
        "body": json.dumps({
            "answer": answer
        },ensure_ascii=False)
    }

合わせて、requirements.txtを以下のように書き換えています。

sam-bedrock-app/bedrock_app/requirements.txt
boto3==1.33.11
langchain==0.0.348

(2024/04/11追記)

Claude3で以下のように見直しました。

sam-bedrock-app/bedrock_app/app.py
import json
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
from langchain_core.prompts import ChatPromptTemplate

def lambda_handler(event, context):
    print(json.dumps(event))
    
    template = ChatPromptTemplate.from_messages([
        ("human", "{country}の首都は?")
    ])
    
    country = event["queryStringParameters"]["country"]
    
    chat = BedrockChat(
        region_name='us-east-1',
        model_id='anthropic.claude-3-sonnet-20240229-v1:0',
        model_kwargs={"temperature": 0.0}
    )
    
    response = chat.invoke(template.invoke({"country": country}))
    answer = response.content
    
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json;charset=UTF-8",
        },
        "body": json.dumps({
            "answer": answer
        },ensure_ascii=False)
    }
sam-bedrock-app/bedrock_app/requirements.txt
boto3==1.34.80
langchain==0.1.14
langchain-community==0.0.31
langchain-core==0.1.41

単体テストのコードを以下のように書き換えました。
具体的には、「"queryStringParameters": {"country": "アメリカ"},」と
「assert "answer" in ret["body"], assert "ワシントン" in data["answer"]」の部分です。

sam-bedrock-app/tests/unit/test_handler.py
import json

import pytest

from bedrock_app import app


@pytest.fixture()
def apigw_event():
    """ Generates API GW Event"""

    return {
        "body": '{ "test": "body"}',
        "resource": "/{proxy+}",
        "requestContext": {
            "resourceId": "123456",
            "apiId": "1234567890",
            "resourcePath": "/{proxy+}",
            "httpMethod": "POST",
            "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
            "accountId": "123456789012",
            "identity": {
                "apiKey": "",
                "userArn": "",
                "cognitoAuthenticationType": "",
                "caller": "",
                "userAgent": "Custom User Agent String",
                "user": "",
                "cognitoIdentityPoolId": "",
                "cognitoIdentityId": "",
                "cognitoAuthenticationProvider": "",
                "sourceIp": "127.0.0.1",
                "accountId": "",
            },
            "stage": "prod",
        },
        "queryStringParameters": {"country": "アメリカ"},
        "headers": {
            "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
            "Accept-Language": "en-US,en;q=0.8",
            "CloudFront-Is-Desktop-Viewer": "true",
            "CloudFront-Is-SmartTV-Viewer": "false",
            "CloudFront-Is-Mobile-Viewer": "false",
            "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
            "CloudFront-Viewer-Country": "US",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Upgrade-Insecure-Requests": "1",
            "X-Forwarded-Port": "443",
            "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
            "X-Forwarded-Proto": "https",
            "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==",
            "CloudFront-Is-Tablet-Viewer": "false",
            "Cache-Control": "max-age=0",
            "User-Agent": "Custom User Agent String",
            "CloudFront-Forwarded-Proto": "https",
            "Accept-Encoding": "gzip, deflate, sdch",
        },
        "pathParameters": {"proxy": "/examplepath"},
        "httpMethod": "POST",
        "stageVariables": {"baz": "qux"},
        "path": "/examplepath",
    }


def test_lambda_handler(apigw_event):

    ret = app.lambda_handler(apigw_event, "")
    data = json.loads(ret["body"])

    assert ret["statusCode"] == 200
    assert "answer" in ret["body"]
    assert "ワシントン" in data["answer"]
    
    print(data["answer"])

regionを環境変数で取得できるようにした点と、test_api_gatewayメソッドを変更しています。

sam-bedrock-app/tests/integration/test_api_gateway.py
import os
import json

import boto3
import pytest
import requests

"""
Make sure env variable AWS_SAM_STACK_NAME exists with the name of the stack we are going to test. 
"""


class TestApiGateway:

    @pytest.fixture()
    def api_gateway_url(self):
        """ Get the API Gateway URL from Cloudformation Stack outputs """
        stack_name = os.environ.get("AWS_SAM_STACK_NAME")

        if stack_name is None:
            raise ValueError('Please set the AWS_SAM_STACK_NAME environment variable to the name of your stack')
            
        """ GetREGIONN """
        region_name = os.environ.get("AWS_REGION")

        if region_name is None:
            raise ValueError('Please set the AWS_REGION environment variable to the name of your region')

        client = boto3.client("cloudformation", region_name=region_name)

        try:
            response = client.describe_stacks(StackName=stack_name)
        except Exception as e:
            raise Exception(
                f"Cannot find stack {stack_name} \n" f'Please make sure a stack with the name "{stack_name}" exists'
            ) from e

        stacks = response["Stacks"]
        stack_outputs = stacks[0]["Outputs"]
        api_outputs = [output for output in stack_outputs if output["OutputKey"] == "BedrockAppApi"]

        if not api_outputs:
            raise KeyError(f"BedrockAppAPI not found in stack {stack_name}")

        return api_outputs[0]["OutputValue"]  # Extract url from stack outputs

    def test_api_gateway(self, api_gateway_url):
        """ Call the API Gateway endpoint and check the response """
        response = requests.get(api_gateway_url + "?country=イギリス")
        
        print(json.dumps(response.json(),ensure_ascii=False))

        assert response.status_code == 200
        assert "ロンドン" in response.json()["answer"]

テストで利用するrequirements.txtも変更しています。

sam-bedrock-app/tests/requirements.txt
pytest
boto3==1.33.11
langchain==0.0.348

次にSAMをビルドします。
ローカルのPython(3.8)とLambdaのPython(3.9)のバージョンが異なるため、--use-containerを使っています。
pytestで事前にテストするため、実際はローカルのPythonとLambdaのPythonのバージョンは合わせた方がよいです。

$ sam validate
/home/ec2-user/environment/bedrock/sam-bedrock-app/template.yaml is a valid SAM Template

$ sam build --use-container
Starting Build use cache                                                                                                                                                                           
Starting Build inside a container                                                                                                                                                                  
Cache is invalid, running build and copying resources for following functions (BedrockAppFunction)                                                                                                 
Building codeuri: /home/ec2-user/environment/bedrock/sam-bedrock-app/bedrock_app runtime: python3.9 metadata: {} architecture: x86_64 functions: BedrockAppFunction                                

Fetching public.ecr.aws/sam/build-python3.9:latest-x86_64 Docker container image..........................................................................................................................................................................................................................................................................................................................................................................................................................
Mounting /home/ec2-user/environment/bedrock/sam-bedrock-app/bedrock_app as /tmp/samcli/source:ro,delegated, inside runtime container                                                               
 Running PythonPipBuilder:ResolveDependencies
 Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided

sam local invokeで試すこともできますが、回答の日本語がエンコードされたままになりました。
解決方法はわかりませんでした。

$ sam local invoke --event events/event.json BedrockAppFunction
Invoking app.lambda_handler (python3.9)                                                                                                                                                            
Local image is up-to-date                                                                                                                                                                          
Using local image: public.ecr.aws/lambda/python:3.9-rapid-x86_64.                                                                                                                                  
                                                                                                                                                                                                   
Mounting /home/ec2-user/environment/bedrock/sam-bedrock-app/.aws-sam/build/BedrockAppFunction as /var/task:ro,delegated, inside runtime container                                                  
{"body": "{\"message\": \"hello world\"}", "resource": "/hello", "path": "/hello", "httpMethod": "GET", "isBase64Encoded": false, "queryStringParameters": {"country": "America"}, "pathParameters": {"proxy": "/path/to/resource"}, "stageVariables": {"baz": "qux"}, "headers": {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, sdch", "Accept-Language": "ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7", "Cache-Control": "max-age=0", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "US", "Host": "1234567890.execute-api.us-east-1.amazonaws.com", "Upgrade-Insecure-Requests": "1", "User-Agent": "Custom User Agent String", "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "requestContext": {"accountId": "123456789012", "resourceId": "123456", "stage": "prod", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "requestTime": "09/Apr/2015:12:34:56 +0000", "requestTimeEpoch": 1428582896000, "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "accessKey": null, "sourceIp": "127.0.0.1", "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Custom User Agent String", "user": null}, "path": "/prod/hello", "resourcePath": "/hello", "httpMethod": "POST", "apiId": "1234567890", "protocol": "HTTP/1.1"}}
{"statusCode": 200, "headers": {"Content-Type": "application/json;charset=UTF-8"}, "body": "{\"answer\": \" \u30a2\u30e1\u30ea\u30ab\u5408\u8846\u56fd\u306e\u9996\u90fd\u306f\u30ef\u30b7\u30f3\u30c8\u30f3D.C.\u3067\u3059\u3002\"}"}END RequestId: 34af76e5-5f01-4aff-9cdb-d2689f960e20
REPORT RequestId: 34af76e5-5f01-4aff-9cdb-d2689f960e20  Init Duration: 0.40 ms  Duration: 14756.89 ms   Billed Duration: 14757 ms       Memory Size: 128 MB     Max Memory Used: 128 MB

events/event.jsonの中身も書き換える必要がありますが、失念してしまいました。
test_handler.pyを参考に書き換えることができると思いますが、分かる方は試してください。

ここまで準備できればデプロイをします。
※samconfig.tomlがない場合は、--guidedオプションをつけて作成します。
実行時にcurl inet-ip.infoでグローバルIPを取得してパラメータを上書きしています。

$ sam deploy --region ap-northeast-1 --parameter-overrides SourceIp=`curl inet-ip.info`

Cloud9でプロファイルやリージョン(configなど)を設定していない場合は、--regionオプションをつけないと以下のエラーが出ます。

$ sam deploy
Error: Error Setting Up Managed Stack Client: Unable to resolve a region. Please provide a region via the --region parameter or by the AWS_REGION environment variable.

デプロイしたAPI Gatewayに対してCurlでテストします。
以下のように日本語をそのまま指定すると内部で文字化けしてしまいます。

$ curl https://******.execute-api.ap-northeast-1.amazonaws.com/Prod/bedrock_app?country=イギリス
{"answer": " 申し訳ありませんが、そのような国や地名は存在しないと思います。"}

-GオプションでGETであることを明示し、--data-urlencodeオプションを使用することで日本語を渡すことができます。

$ curl -G https://******.execute-api.ap-northeast-1.amazonaws.com/Prod/bedrock_app --data-urlencode country="イギリス"
{"answer": " イギリスの首都はロンドンです。"}

-Gオプションを指定しない場合は、以下のエラーが出ます。

{"message":"Missing Authentication Token"}

問題がなければ、以下のように単体テスト、結合テストをパスできます。

$ AWS_SAM_STACK_NAME=sam-bedrock-app AWS_REGION=ap-northeast-1 pytest -s
======================================================================================= test session starts =======================================================================================
platform linux -- Python 3.8.16, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/ec2-user/environment/bedrock/sam-bedrock-app/tests
plugins: anyio-4.1.0
collected 2 items                                                                                                                                                                                 

integration/test_api_gateway.py {"answer": " イギリスの首都はロンドンです。"}
.
unit/test_handler.py {"body": "{ \"test\": \"body\"}", "resource": "/{proxy+}", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/{proxy+}", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "identity": {"apiKey": "", "userArn": "", "cognitoAuthenticationType": "", "caller": "", "userAgent": "Custom User Agent String", "user": "", "cognitoIdentityPoolId": "", "cognitoIdentityId": "", "cognitoAuthenticationProvider": "", "sourceIp": "127.0.0.1", "accountId": ""}, "stage": "prod"}, "queryStringParameters": {"country": "\u30a2\u30e1\u30ea\u30ab"}, "headers": {"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", "Accept-Language": "en-US,en;q=0.8", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Mobile-Viewer": "false", "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "CloudFront-Viewer-Country": "US", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Upgrade-Insecure-Requests": "1", "X-Forwarded-Port": "443", "Host": "1234567890.execute-api.us-east-1.amazonaws.com", "X-Forwarded-Proto": "https", "X-Amz-Cf-Id": "aaaaaaaaaae3VYQb9jd-nvCd-de396Uhbp027Y2JvkCPNLmGJHqlaA==", "CloudFront-Is-Tablet-Viewer": "false", "Cache-Control": "max-age=0", "User-Agent": "Custom User Agent String", "CloudFront-Forwarded-Proto": "https", "Accept-Encoding": "gzip, deflate, sdch"}, "pathParameters": {"proxy": "/examplepath"}, "httpMethod": "POST", "stageVariables": {"baz": "qux"}, "path": "/examplepath"}
 アメリカの首都はワシントンD.C.です。
.

======================================================================================== 2 passed in 5.35s ========================================================================================

リソースの削除

テストが終われば、以下のコマンドでリソースを削除します。

$ sam delete sam-bedrock-app --region ap-northeast-1
        Are you sure you want to delete the stack sam-bedrock-app in the region ap-northeast-1 ? [y/N]: y
        Do you want to delete the template file 1ff43587e6dbeacfc892df10d130b26d.template in S3? [y/N]: y
        - Deleting S3 object with key e5d44a92726031af84503c42d9a266bd                                                                                                                             
        - Deleting S3 object with key 1ff43587e6dbeacfc892df10d130b26d.template                                                                                                                    
        - Deleting Cloudformation stack sam-bedrock-app

Deleted successfully
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?