Introduction
先日、自動でコード修正するAgentをする記事を書きました。
ここではTypescriptを使ってBedrockにアクセスしていたのですが、
今回はRustでAmazon Bedrockにアクセスしてみます。
Environments
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 14.3.1
- Rust : 1.76.0
- aws-cli : 2.15.32
※AWS アカウントは使用可能とします
Setup
Bedrockの準備はこのへんを参考に使えるようにしておきます。
次にCargoで適当なRustプロジェクトを作成して必要なcrateを追加しておきましょう。
% cargo new bedrock-app
% cd bedrock-app
% cargo add aws-config aws-sdk-bedrock aws-sdk-bedrockruntime
% cargo add serde
% cargo add serde-json
Cargo.tomlはこんな感じです。
[dependencies]
aws-config = { version = "1.1.8", features = ["behavior-version-latest"] }
aws-sdk-bedrock = "1.18.0"
aws-sdk-bedrockruntime = "1.18.0"
aws-smithy-runtime-api = "1.2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.114"
tokio = { version = "1", features = ["full"] }
Try
ではBedrockへRustからアクセスしてみます。
main.rs を編集していきましょう。
必要なモジュールをimportしてmain関数でaws_configを作成します。
Claude3モデルを使いたい場合、現状ではus-east-1リージョンだけなので
それを指定します。
定義している構造体はbedrockに渡すための情報です。
use aws_config::meta::region::RegionProviderChain;
use aws_sdk_bedrock::error::SdkError;
use aws_sdk_bedrockruntime::operation::invoke_model::builders::InvokeModelFluentBuilder;
use aws_sdk_bedrockruntime::operation::invoke_model::{InvokeModelError, InvokeModelOutput};
use use aws_sdk_bedrockruntime::primitives::Blob;
use std::borrow::Cow;
use serde_json::Value;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Message {
role: String,
content: Vec<Content>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Content {
r#type: String,
text: String,
}
#[derive(Serialize, Deserialize, Debug)]
struct Payload {
anthropic_version: String,
max_tokens: u32,
messages: Vec<Message>,
temperature: f32,
top_p: f32,
}
#[tokio::main]
async fn main() {
let region = Region::new("us-east-1");
let config = aws_config::from_env().region(region).load().await;
invoke_bedrock(&config, "Rust言語について100文字以内で簡単に教えて下さい。").await;
}
invoke_bedrock の第2引数が prompt です。
この関数ではクライアントを作成し、invoke_model で InvokeModelFluentBuilder を取得して
実行に必要な情報を設定します。
model_id には使いたいモデルの ID を設定します。
ドキュメントによると body と model_id さえ指定すれば動くみたいですが、
content_type を明示的に設定しないと動かなかったので設定します。
send 関数を実行することで実際に Bedrock に prompt を post します。
async fn invoke_bedrock(config: &aws_config::SdkConfig, prompt: &str) {
let runtime = aws_sdk_bedrockruntime::Client::new(&config);
let builder: InvokeModelFluentBuilder = runtime.invoke_model();
//Payload情報を作成してBlog型のBodyを作成
let payload = build_payload(prompt, 512);
let payload_json = serde_json::to_vec(&payload).unwrap();
let body: Blob = Blob::new(payload_json);
let output = builder
.model_id("anthropic.claude-3-sonnet-20240229-v1:0")
.body(body)
.content_type("application/json")
.send()
.await;
handle_output(output);
}
fn build_payload(prompt: &str, max_tokens: u32) -> Payload {
Payload {
anthropic_version: "bedrock-2023-05-31".to_string(),
max_tokens: max_tokens,
messages: vec![Message {
role: "user".to_string(),
content: vec![Content {
r#type: "text".to_string(),
text: prompt.to_string(),
}],
}],
temperature: 0.5,
top_p: 0.9,
}
}
あとは結果を表示するだけです。
fn handle_output(
invoke_model_output: Result<
InvokeModelOutput,
SdkError<InvokeModelError, ::aws_smithy_runtime_api::client::orchestrator::HttpResponse>,
>,
) {
match invoke_model_output {
Ok(output) => {
let response_body = String::from_utf8_lossy(&output.body.as_ref());
println!("Response: {}", response_body);
}
Err(err) => {
eprintln!("Bedrock Error: {:?}", err);
}
}
}
実行すると下記のように表示されます。
% cargo run
Response: {"id":"msg_01XXXXXXXX","type":"message","role":"assistant",
"content":[{"type":"text","text":"Rustは、システムプログラミング言語で、メモリ安全性、並列性、
パフォーマンスに優れています。所有権の概念により、ランタイムのメモリ安全性を保証します。関数型とオブジェクト指向の特徴を併せ持ち、
コンパイル時に多くのエラーを検出できます。Mozillaによって開発され、
システムソフトウェア、Webブラウザエンジン、オペレーティングシステムなどに利用されています。"}],
"model":"claude-3-sonnet-28k-20240229","stop_reason":"end_turn",
"stop_sequence":null,"usage":{"input_tokens":29,"output_tokens":158}}
Stream
Stream でレスポンスを受け取りたい場合、invoke_model_with_response_stream 関数を使って
builder を取得します。
あとはそのまま。
async fn invoke_bedrock_stream(config: &aws_config::SdkConfig, prompt: &str) {
let runtime = aws_sdk_bedrockruntime::Client::new(&config);
//Stream用
let builder: InvokeModelWithResponseStreamFluentBuilder =
runtime.invoke_model_with_response_stream();
let payload = build_payload(prompt, 512);
let payload_json = serde_json::to_vec(&payload).unwrap();
let body: Blob = Blob::new(payload_json);
let output = builder
.model_id("anthropic.claude-3-sonnet-20240229-v1:0")
.body(body)
.content_type("application/json")
.send()
.await;
handle_output_stream(output).await;
}
レスポンスの処理方法がさきほどと違います。
InvokeModelWithResponseStreamOutput の body をループしながら
recv()で順次 Stream の Chunk をうけとっていきます。
async fn handle_output_stream(
invoke_model_output: Result<
InvokeModelWithResponseStreamOutput,
SdkError<InvokeModelWithResponseStreamError,
::aws_smithy_runtime_api::client::orchestrator::HttpResponse,
>,
>,
) {
match invoke_model_output {
Ok(output) => {
let mut response_stream = output.body;
loop {
match response_stream.recv().await {
Ok(Some(aws_sdk_bedrockruntime::types::ResponseStream::Chunk(
payload_part,
))) => {
if let Some(blob) = &payload_part.bytes {
let data: Cow<'_, str> = String::from_utf8_lossy(&blob.as_ref());
let value: Value = serde_json::from_str(&data).unwrap();
if value["type"] == "content_block_delta" {
if let Some(delta) = value["delta"].as_object() {
if let Some(text) = delta["text"].as_str() {
println!("{}", text);
}
}
}
}
}
Err(err) => {
println!("Stream Error");
}
Ok(None) => {
println!("Stream End");
break;
}
Ok(Some(_)) => {
println!("other case");
}
}
}
}
Err(err) => {
eprintln!("Bedrock Error: {:?}", err);
}
}
}
ちなみに、生成AIに実装方法を聞くと、しつこくrecvでなくnext(そんな関数はない)を呼べといってくる。
invoke_bedrock_stream を呼ぶように修正して実行すと、
Chunk ごとに文字が表示されていきます。