Go + Amazon Bedrock Claude 3 使ってみる

Claude3がどうもGPTより良いと評判になっているので気になって使ってみる事にしました。Goで書いてみたので備忘録も兼ねて残します！

https://aws.amazon.com/jp/about-aws/whats-new/2024/03/anthropics-claude-3-sonnet-model-amazon-bedrock/

## やってみる
Bedrock + Claude3-sonnetが使用可能であり、必要なアクセス権も付与済みである事を前提とします。

まず、`.env`を準備します。私の場合は`us-east-1`に`Claude3-sonnet`を準備しました。

```.env
AWS_ACCESS_KEY_ID=<アクセスキーID>
AWS_SECRET_ACCESS_KEY=<シークレットキー>
AWS_DEFAULT_REGION=us-east-1
```

### 通常のメッセージ

以下は通常のメッセージAPIです。
[GoのSDKの情報](https://docs.aws.amazon.com/ja_jp/code-library/latest/ug/go_2_bedrock-runtime_code_examples.html)は一部Claude2の時の内容っぽい感じだったので、以下のドキュメントを参考にしました。

https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html

モデルIDは以下から確認できます。

https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html

```go:main.go
package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"

	"github.com/joho/godotenv"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	for {

		fmt.Println("Enter your message:")
		userMessage, err := reader.ReadString('\n')
		if err != nil {
			log.Fatalf("Failed to read from stdin: %v", err)
		}

		userMessage = strings.TrimSpace(userMessage)
		systemPrompt := "あなたは魚大好き星人です。なんでも魚に例えて話してみてください"
		response, err := claudeMessageCompletion(userMessage, systemPrompt)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}
		fmt.Println("Response:", response)
	}
}

func claudeMessageCompletion(userMessage string, systemPrompt string) (string, error) {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	cfg, err := config.LoadDefaultConfig(context.TODO(),
		config.WithRegion("us-east-1"))
	if err != nil {
		return "", fmt.Errorf("error Config Load: %v", err)
	}

	// Bedrockクライアントを初期化
	brc := bedrockruntime.NewFromConfig(cfg)

	content := Content{Type: "text", Text: userMessage}

	msg := Message{Role: "user", Content: []Content{content}}

	payload := Request{
		AnthropicVersion: "bedrock-2023-05-31",
		MaxTokens:        512,
		System:           systemPrompt,
		Messages:         []Message{msg},
		Temperature:      0.5,
		TopP:             0.9,
	}

	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("error marshalling payload: %v", err)
	}

	output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{
		Body:        payloadBytes,
		ModelId:     aws.String("anthropic.claude-3-sonnet-20240229-v1:0"),
		ContentType: aws.String("application/json"),
	})
	if err != nil {
		return "", fmt.Errorf("error Claude response: %v", err)
	}

	var resp Response

	err = json.Unmarshal(output.Body, &resp)
	if err != nil {
		return "", fmt.Errorf("error unmarshalling response: %v", err)
	}

	return resp.ContentItem[len(resp.ContentItem)-1].Text, nil

}

type Request struct {
	AnthropicVersion string    `json:"anthropic_version"`
	MaxTokens        int       `json:"max_tokens"`
	System           string    `json:"system"`
	Messages         []Message `json:"messages"`
	Temperature      float64   `json:"temperature"`
	TopP             float64   `json:"top_p"`
}

type Message struct {
	Role    string    `json:"role"`
	Content []Content `json:"content"`
}

type Content struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type Response struct {
	ID           string        `json:"id"`
	Model        string        `json:"model"`
	Type         string        `json:"type"`
	Role         string        `json:"role"`
	ContentItem  []ContentItem `json:"content"`
	StopReason   string        `json:"stop_reason,omitempty"`
	StopSequence string        `json:"stop_sequence,omitempty"`
	Usage        UsageDetails  `json:"usage"`
}

type ContentItem struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type UsageDetails struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}
```
「魚大好き星人」ってよく分からないけど、やったのは夜だったのでその時のテンションですね...！
以下のように返ってきました。
```
Enter your message:
今日はいい天気ですね
Response: はい、今日の空は真っ青な大海のようですね。雲は白い波のように優雅に流れています。太陽の光は海面を照らすような明るさで、気持ちの良い1日になりそうです。魚たちも快適に泳ぎ回っているかのように、心地よい風が吹いています。このように晴れ渡った日は、大漁が期待できそうな好機会ですね!

Enter your message:
なんか面白い事言って             
Response: はい、魚大好き星人として、すべてを魚に例えて話してみますね!

人生は大海原のようなものです。時には穏やかな波のように平穏に過ごせる時もありますが、時には激しい嵐のような試練に見舞われることもあります。しかし、私たちは強くたくましい魚のように、そうした困難を乗り越えていかなければなりません。

恋愛も魚の産卵のようなものかもしれません。雄と雌が出会い、一緒に泳ぎ、子孫を残すため卵を産みます。しかし、その卵が無事に孵るかどうかは運次第です。時に捕食者に食べられてしまうかもしれません。

仕事は漁に例えられるでしょう。漁師は一生懸命漁をして、良い獲物を手に入れようと努力します。しかし、時には何も獲れない日もあります。それでも諦めずに続けることが大切なのです。

このように、魚に例えれば人生のさまざまな側面を表現できますね。波乱万丈な大海原を泳ぎ続けることが私たちの使命なのかもしれません。
```

### ストリームの場合

次にストリームによるテキスト生成を試してみます。
サンプルコードを参考に以下で動きました。

https://docs.aws.amazon.com/ja_jp/code-library/latest/ug/go_2_bedrock-runtime_code_examples.html

```go:main.go
package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types"
	"github.com/joho/godotenv"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	cfg, err := config.LoadDefaultConfig(context.TODO(),
		config.WithRegion("us-east-1"))
	if err != nil {
		log.Fatalf("error Config Load: %v", err)
	}

	brc := bedrockruntime.NewFromConfig(cfg)

	systemPrompt := "あなたは猫です"

	for {
		fmt.Printf("Enter your message >>\n")
		userMessage, err := reader.ReadString('\n')
		if err != nil {
			log.Fatalf("Failed to read from stdin: %v", err)
		}

		userMessage = strings.TrimSpace(userMessage)
		content := Content{Type: "text", Text: userMessage}
		msg := Message{Role: "user", Content: []Content{content}}

		request := Request{
			AnthropicVersion: "bedrock-2023-05-31",
			MaxTokens:        512,
			System:           systemPrompt,
			Messages:         []Message{msg},
			Temperature:      0.5,
			TopP:             0.9,
		}

		body, err := json.Marshal(request)
		if err != nil {
			log.Panicln("Couldn't marshal the request: ", err)
		}

		output, err := brc.InvokeModelWithResponseStream(context.Background(), &bedrockruntime.InvokeModelWithResponseStreamInput{
			Body:        body,
			ModelId:     aws.String("anthropic.claude-3-sonnet-20240229-v1:0"),
			ContentType: aws.String("application/json"),
		})

		if err != nil {
			errMsg := err.Error()
			if strings.Contains(errMsg, "no such host") {
				log.Printf("The Bedrock service is not available in the selected region.")
			} else if strings.Contains(errMsg, "Could not resolve the foundation model") {
				log.Printf("Could not resolve the foundation model from model identifier")
			} else {
				log.Printf("Couldn't invoke Anthropic Claude. Here's why: %v\n", err)
			}
		}

		err = processStreamingOutput(output, func(ctx context.Context, part []byte) error {
			if string(part) == "" {
				fmt.Print("\n")
				return nil
			}
			fmt.Print(string(part))
			return nil
		})
		if err != nil {
			log.Fatal("streaming output processing error: ", err)
		}
	}
}

type StreamingOutputHandler func(ctx context.Context, part []byte) error

func processStreamingOutput(output *bedrockruntime.InvokeModelWithResponseStreamOutput, handler StreamingOutputHandler) error {

	var combinedResult string

	for event := range output.GetStream().Events() {
		switch v := event.(type) {
		case *types.ResponseStreamMemberChunk:

			// fmt.Println("payload", string(v.Value.Bytes))
			var resp Response

			err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(&resp)
			if err != nil {
				return err
			}

			err = handler(context.Background(), []byte(resp.Delta.Text))
			if err != nil {
				return err
			}

			combinedResult += resp.Delta.Text

		case *types.UnknownUnionMember:
			fmt.Println("unknown tag:", v.Tag)

		default:
			fmt.Println("union is nil or unknown type")
		}
	}

	return nil
}

type Request struct {
	AnthropicVersion string    `json:"anthropic_version"`
	MaxTokens        int       `json:"max_tokens"`
	System           string    `json:"system"`
	Messages         []Message `json:"messages"`
	Temperature      float64   `json:"temperature"`
	TopP             float64   `json:"top_p"`
}

type Message struct {
	Role    string    `json:"role"`
	Content []Content `json:"content"`
}

type Content struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type Response struct {
	Type  string    `json:"type"`
	Index int       `json:"index"`
	Delta TextDelta `json:"delta"`
}

type TextDelta struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

```

### マルチモーダル 画像の解析

最後に、画像の解析もお願いしてみました！
```go:main.go
package main

import (
	"bufio"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"

	"github.com/joho/godotenv"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	filePath := "image.png" //画像のファイルパス
	fmt.Println("Enter your message:")
	userMessage, err := reader.ReadString('\n')
	if err != nil {
		log.Fatalf("Failed to read from stdin: %v", err)
	}
	userMessage = strings.TrimSpace(userMessage)
	systemPrompt := "この画像を見て、何が写っているか説明してください。"
	response, err := claudeImgAnalysisCompletion(userMessage, systemPrompt, filePath)
	if err != nil {
		log.Fatalf("Error: %v", err)
	}
	fmt.Println("Response:", response)
}

func claudeImgAnalysisCompletion(userMessage string, systemPrompt string, filePath string) (string, error) {
	err := godotenv.Load(".env")
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	cfg, err := config.LoadDefaultConfig(context.TODO(),
		config.WithRegion("us-east-1"))
	if err != nil {
		return "", fmt.Errorf("error Config Load: %v", err)
	}

	brc := bedrockruntime.NewFromConfig(cfg)

	base64Image, err := encodeImageToBase64(filePath)
	if err != nil {
		log.Fatalf("Failed to encode image: %v", err)
	}

	contentImage := Content{
		Type: "image",
		Source: &Source{
			Type:      "base64",
			MediaType: "image/jpeg",
			Data:      base64Image,
		},
	}

	contentText := Content{
		Type: "text",
		Text: userMessage,
	}

	message := Message{
		Role:    "user",
		Content: []Content{contentImage, contentText},
	}

	payload := Request{
		AnthropicVersion: "bedrock-2023-05-31",
		MaxTokens:        1024,
		System:           systemPrompt,
		Messages:         []Message{message},
	}

	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("error marshalling payload: %v", err)
	}

	output, err := brc.InvokeModel(context.Background(), &bedrockruntime.InvokeModelInput{
		Body:        payloadBytes,
		ModelId:     aws.String("anthropic.claude-3-sonnet-20240229-v1:0"),
		ContentType: aws.String("application/json"),
	})
	if err != nil {
		return "", fmt.Errorf("error Claude response: %v", err)
	}

	var resp Response

	err = json.Unmarshal(output.Body, &resp)
	if err != nil {
		return "", fmt.Errorf("error unmarshalling response: %v", err)
	}

	return resp.ContentItem[len(resp.ContentItem)-1].Text, nil
}

func encodeImageToBase64(filePath string) (string, error) {
	image, err := os.ReadFile(filePath)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(image), nil
}

type Request struct {
	AnthropicVersion string    `json:"anthropic_version"`
	MaxTokens        int       `json:"max_tokens"`
	System           string    `json:"system"`
	Messages         []Message `json:"messages"`
}

type Message struct {
	Role    string    `json:"role"`
	Content []Content `json:"content"`
}

type Content struct {
	Type   string  `json:"type"`
	Text   string  `json:"text,omitempty"`
	Source *Source `json:"source,omitempty"`
}

type Source struct {
	Type      string `json:"type"`
	MediaType string `json:"media_type"`
	Data      string `json:"data"`
}

type Response struct {
	ID           string        `json:"id"`
	Model        string        `json:"model"`
	Type         string        `json:"type"`
	Role         string        `json:"role"`
	ContentItem  []ContentItem `json:"content"`
	StopReason   string        `json:"stop_reason,omitempty"`
	StopSequence string        `json:"stop_sequence,omitempty"`
	Usage        UsageDetails  `json:"usage"`
}

type ContentItem struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type UsageDetails struct {
	InputTokens  int `json:"input_tokens"`
	OutputTokens int `json:"output_tokens"`
}
```
画像ファイルはかわいいこの画像！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2821082/696ba09c-2286-24e3-4cac-5b55481c8627.png)

以下、返答！
```
Enter your message:
この画像はなんだろう

Response: この画像には愛らしい光景が映っています。
大きな犬と小さな子猫が仲良く並んで座っています。
犬は優しげな表情で子猫のそばにいます。子猫は犬に寄り添うようにしています。
そして2匹ともリラックスした様子で、家族のように見えます。
この温かい絵は動物同士の素敵な絆を表しているように思えます。
個々の個性は明かさずに、この友好的な雰囲気をお伝えしました。
```

## さいごに
まだ少ししか触ってないのですが、感覚的にはすごく良い感じです！
GPTじゃなくても全然良いかなって思ってきました。

今度、このLLMはなんでしょうクイズ大会でもやろうかな。絶対分からない気がする😁

以上です。拙い内容で恐縮ですが最後までご覧いただきありがとうございました！