SNSを用いたSlackメッセージ送信ワークフローをStep Functionsで作ってみた（with Bedrock）

## はじめに
AWS Step Functionsを使うと簡単にワークフロージョブを作成することができます。
今回は最近流行りの生成AIサービスBedrockをワークフローの間に噛ませて、Bedrockで生成したテキストをSlackにメッセージ送信するまでを作ってみました。

## ポイント
- Step FunctionsからAWSサービスの直接呼び出し
- JSONataを使った値の参照・抽出と変数の使用方法
- Amazon SNSとAWS ChatBotを使ったメッセージ送信（ほぼ別記事参照）

がつかめる記事になっているはずです。


### 準備・前提

- リージョン：バージニア北部
- Slackのワークスペース・チャンネル作成済み
- ChatBotとSNSの設定済み

のもと、進めていきます。
ChatBotとSNSについては、下記のサイトを参考にして作成できます。

https://aws.amazon.com/jp/builders-flash/202006/slack-chatbot/

ChatBotのコンソール上の`テストメッセージを送信`を押下し、Slackにメッセージが届いていれば準備は完了です。

![スクリーンショット 2024-12-30 22.56.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/94fda165-20ff-6760-efc9-53b5a248cfa5.png)

![スクリーンショット 2024-12-30 22.44.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/e8df75e3-be65-31e2-dc9d-3720e1793fb6.png)


### Step Functions

ここからStep Functionsの解説です。

今回はStep Functionsのステートマシン実行入力で、翻訳させたいテキスト`text`と翻訳言語`language`を渡してあげて、Slackに「元テキスト」・「翻訳言語」・「翻訳後テキスト」をメッセージで送信するワークフローにしました。

```json
{
  "text": "こんにちは",
  "language": "イタリア語"
}
```

ワークフローは2ステップの簡単なものです。

![stepfunctions_graph (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/1ef7a26b-0069-d665-5d07-58b83fd9e6c1.png)


最終的なステートマシンのコードはこちらです。

```json
{
  "QueryLanguage": "JSONata",
  "Comment": "A description of my state machine",
  "StartAt": "Bedrock InvokeModel",
  "States": {
    "Bedrock InvokeModel": {
      "Type": "Task",
      "Resource": "arn:aws:states:::bedrock:invokeModel",
      "Arguments": {
        "ModelId": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0",
        "Body": {
          "anthropic_version": "bedrock-2023-05-31",
          "max_tokens": 200,
          "messages": [
            {
              "role": "user",
              "content": [
                {
                  "type": "text",
                  "text": "{% $states.input.text & 'を' & $states.input.language & 'に翻訳して出力して。出力は翻訳した文だけにしてください。' %}"
                }
              ]
            }
          ]
        }
      },
      "Output": {
        "text": "{% $states.result.Body.content[0].text %}"
      },
      "Next": "SNS Publish",
      "Assign": {
        "inputText": "{% $states.input.text %}",
        "inputLanguage": "{% $states.input.language %}"
      }
    },
    "SNS Publish": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "End": true,
      "Arguments": {
        "TopicArn": "arn:aws:sns:us-east-1:654654550185:BedrockStepFunctionTestTopic",
        "Message": {
          "version": "1.0",
          "source": "custom",
          "content": {
            "description": "{% '原文:' & $inputText & '\n\n' & '言語:' & $inputLanguage & '\n\n' & '翻訳結果:' & $states.input.text %}"
          }
        }
      }
    }
  }
}
```

:::note warn
Step FunctionsからBedrockをinvokeするIAM RoleとSNS PublishするIAM Roleも作成が必要です。
:::

### Bedrock invoke部分
#### 入力からモデルパラメータへの設定まで
![スクリーンショット 2024-12-30 23.15.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/f5ff995c-8253-f756-306a-ed38b44dcfee.png)

Bedock InvokeModelをワークフローに追加し、`anthropic.claude-3-sonnet-20240229-v1:0"`のモデルを選んだ際のBedrock モデルパラメータは下記のJSONになっています。

```json
{
  "anthropic_version": "bedrock-2023-05-31",
  "max_tokens": 200,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "string"
        }
      ]
    }
  ]
}
```

まずはお試しでcontentの配列のオブジェクト`text`のvalueである`"string"`部分を書き換えて、
`"こんにちはを英語で翻訳してください"`

と入力し、保存。
この状態で動かすだけでもBedrock invokeされて翻訳結果がステートの結果として出力できるはずです。

ただ今回はステートマシンの入力で動的にvalue部分を書き換えて、毎回違う言葉を違う言語に翻訳できるようにしてみたいです。

```json
{
  "text": "こんにちは",
  "language": "イタリア語"
}
```
そこでJSONataで入力値参照を行います。

```json
{
  "anthropic_version": "bedrock-2023-05-31",
  "max_tokens": 200,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "{% $states.input.text & 'を' & $states.input.language & 'に翻訳して出力して。' %}"
        }
      ]
    }
  ]
}
```

Step FunctionにおけるJSONata記法では、下記のように外側を`"{% %}"`で囲って、その中で値の参照や演算を行うことが可能です。

```json
"{% $here.JSONata.expression %}"
```

また、ステートに渡される入力は
```
$states.input.hoge
```
で参照することができます。今回の場合、`text`というKeyをステートマシンの入力で渡すので、`$states.input.text`で参照をしています。

JSONataではテンプレートリテラル的な変数との結合を行うこともできます。

`$states.input.text & 'を'`
のように、
- `$states.input.text`
- `を`

を、`&`を用いて結合しています。

```json
{
  "text": "こんにちは",
  "language": "イタリア語"
}
```

の実行入力の場合、Bedrock invokeに渡される"text"のvalueは、最終的に`こんにちはをイタリア語に翻訳して出力して。`となります。

#### Bedrock invoke結果の出力
Bedrock invokeで得られる出力はデフォルトだと下記のようになっていました。

```json
{
  "Body": {
    "id": "msg_bdrk_01FYBMe2qw3UA1DCvUdGjeem",
    "type": "message",
    "role": "assistant",
    "model": "claude-3-sonnet-20240229",
    "content": [
      {
        "type": "text",
        "text": "こんにちはのスペイン語訳は:\n\nBuenas tardes\n\nです。"
      }
    ],
    "stop_reason": "end_turn",
    "stop_sequence": null,
    "usage": {
      "input_tokens": 28,
      "output_tokens": 27
    }
  },
  "ContentType": "application/json"
}
```

ただ、今後のステートで、使われたモデル情報やid等は不要であり、翻訳の内容だけ得たいためその部分だけを次のSNSステートに出力するよう、出力内容を加工してみます。

上記のデータから、欲しいのは、`Body→content→配列の最初のオブジェクト→text`という階層を確認しておきます。

それをJSONataで表現すると、

```json
"{% $states.result.Body.content[0].text %}"
```

になります。`$states.result`でステートの出力にアクセスでき、その後は`Body`、`content`のindexが`0`の`text`と、順に参照をしていく形になります。

次のSNSステートには分かりやすいよう下記のようにtextのvalueとして、渡してあげます。

```json
{
  "text": "{% $states.result.Body.content[0].text %}"
}
```

![スクリーンショット 2024-12-30 23.41.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/4132ced5-6166-aebc-1f27-12bf1ff500ce.png)

### SNS部分

毎回固定のメッセージをSlackに送る場合、下記のように`content.description`の値に任意の文字列を設定すると実現できます。

```json
{
  "version": "1.0",
  "source": "custom",
  "content": {
    "description": "こんにちは！"
  }
}
```
今回はBedrockで翻訳されたテキストを動的に送りたいため、JSONataを用いて値の参照を行います。
そして、2024年秋に新しく追加された"変数"機能についても今回扱うようにしてみました。

完成したメッセージの設定が下記です。

```json
{
  "version": "1.0",
  "source": "custom",
  "content": {
    "description": "{% '原文:' & $inputText & '\n\n' & '言語:' & $inputLanguage & '\n\n' & '翻訳結果:' & $states.input.text %}"
  }
}
```

description部分の解説をします。
```json
"{% '原文:' & $inputText & '\n\n' & '言語:' & $inputLanguage & '\n\n' & '翻訳結果:' & $states.input.text %}"
```

いきなりですが、`$inputText`と`$inputLanguage`が変数の参照です。これは前段のBedrock invokeの設定内に記述していたものになります。

```json
  "Assign": {
    "inputText": "{% $states.input.text %}",
    "inputLanguage": "{% $states.input.language %}"
  }
```

変数の設定で、ステートマシン入力値の`text`と`language`を参照し、それぞれ`inputText`という変数と、`inputLanguage`という変数を設定していました。

後述のステートで、この変数は`$inputText`・`$inputLanguage`という形で参照が可能になります。

![スクリーンショット 2024-12-30 23.50.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/c1359f9f-6494-95a3-1ddf-d3fba3c1b740.png)

```json
"{% '原文:' & $inputText & '\n\n' & '言語:' & $inputLanguage & '\n\n' & '翻訳結果:' & $states.input.text %}"
```

description部分のその他はステートへの入力値の参照`$states.input.text`と結合`&`になっています。

ステートマシンの解説はここまでで、実行結果を見てみましょう。

ステートマシンから`実行を開始`を押下し、入力モーダルを開きます。

![スクリーンショット 2024-12-30 23.57.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/f8307501-6ecc-1c6a-e7ec-825fd1dd09b6.png)

下記をフォームに入力し、`実行を開始`を押してみます。

```json
{
  "text": "今年ももう終わるよ、一年あっという間だったね。",
  "language": "英語"
}
```

![スクリーンショット 2024-12-30 23.58.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/1719d90f-d388-cd5f-ca33-4a3984e436c0.png)

するとSlackに新規メッセージが届きました。原文と言語も入力値が出力できていることが
確認できました。

![スクリーンショット 2024-12-31 0.00.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/af624642-7680-4518-c3e4-c65ad90ccd3c.png)

英語で試してみたので、次はフランス語にして実行してみましょう。

```json
{
  "text": "今年ももう終わるよ、一年あっという間だったね。",
  "language": "フランス語"
}
```

するとフランス語で結果が帰ってきました。

![スクリーンショット 2024-12-31 0.04.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/bfdcbed4-0527-9b49-21f6-c0d5568990a4.png)

意図通り、内容を翻訳→出力してメッセージングできていますね！

## おわりに

今回はStep FunctionsからBedrockでテキスト生成し、その結果をSlack通知するまでを実施しました。
Step FunctionsからBedrockをこんなに簡単に実行できるのかという驚きがまずありました。

Lambdaを使わなくてもStep FunctionsでAWSサービスを簡単に呼び出して使用することができるのでとても便利ですね。

また、Bedrockのinvoke結果をフロント（画面やチャットツールなど）に出力する流れはJSONataでデータをゴニョゴニョ操作しがいがあるので、JSONataの勉強になった&楽しかったです！