【Agentforce × Amazon Lex × Amazon Connect】発話内容からSalesforce Agentforceを利用し適切なアクションを実行する

## はじめに

Amazon ConnectとAmazon Lexを使用して、AIチャットボットを作成しました。
発話内容をSalesforceのAgentforceへ渡し、Salesforce内で適切なアクションを実行する方法について記事をまとめました。

構成図は以下の通りです。

![名称未設定ファイル.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/7547ef46-a103-470f-9e09-dbffddbbdc19.png)

## テスト内容について

Agentforceには、顧客の電話番号を使用して過去の注文内容を確認できるようなアクションを設定しています。
今回は、以下の流れで注文内容の確認を電話で行えるかどうかをテストしています。

AIエージェント「質問内容をお伝えください。」
顧客「私の注文について教えてください。」
AIエージェント「お客様の注文を確認するために、電話番号を教えてください」
顧客「080-XXXX-XXXX」
AIエージェント「お客様の注文はXXXXXXXXとなります。他に何か疑問点はありますか？」
顧客「大丈夫です。ありがとうございます。」
AIエージェント「ご利用いただきありがとうございます。よい1日をお過ごしください。」

## Amazon Lexの作成

スロットタイプを `Amazon.FreeFormInput` としたスロットを１つ作成します。

![スクリーンショット 2025-03-13 18.25.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/8cbcd860-69b8-4ac4-8d50-af10e840591f.png)

サンプル発話にはダミーを入れておきます。

![スクリーンショット 2025-03-13 18.26.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/ed127f74-0cbb-4179-89d8-64aa9e65b524.png)

コードフックオプションは有効にします。

![スクリーンショット 2025-03-13 18.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/a2dadb4e-ae7d-4dd2-bb64-a03042c68362.png)

エイリアスのLambda関数には `agentforce-send-message` を設定します。

![スクリーンショット 2025-03-13 18.41.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/28da3861-5e7c-4b0d-9d9d-738d7e69d99f.png)

## AWS Lambdaについて

Lambdaに関しては以下の2つが必要となります。

- Agent APIのセッション開始するLambda
- Agent APIでメッセージを送信するLambda

Agent APIに関しては以下のAgentforce Developer Guideを参照してください。

https://developer.salesforce.com/docs/einstein/genai/guide/agent-api.html

## Agent APIのセッション開始するLambda

ランタイム : Node.js 22.x

```agentforce-create-session.js
import https from 'https';
import { v4 as uuidv4 } from 'uuid';
import querystring from 'querystring';
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

export const handler = async (event) => {
    const agentId = process.env.AGENT_ID;
    const myDomainUrl = process.env.MY_DOMAIN_URL;
    const secretsManagerClient = new SecretsManagerClient({ region: process.env.REGION });

    let sessionId;
    let accessToken;

    try {
        const secret = await getSecret(secretsManagerClient, process.env.SECRET_NAME);
        const secretValues = JSON.parse(secret.SecretString);
        const consumerKey = secretValues.CONSUMER_KEY;
        const consumerSecret = secretValues.CONSUMER_SECRET;

        accessToken = await getAccessToken(myDomainUrl, consumerKey, consumerSecret);
        console.log('Access Token Generated');

        const sessionResponse = await createSession(agentId, accessToken, myDomainUrl);
        sessionId = sessionResponse.sessionId;
        console.log('Session Created:', sessionResponse);

        return {
            statusCode: 200,
            sessionId: sessionId,
            accessToken: accessToken
        };
    } catch (error) {
        console.error('Error:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: error.message }),
        };
    }

    async function getSecret(secretsManagerClient, secretName) {
        const command = new GetSecretValueCommand({
            SecretId: secretName,
        });

        try {
            const response = await secretsManagerClient.send(command);
            return response;
        } catch (error) {
            console.error("Error:", error);
            throw error;
        }
    }

    async function getAccessToken(myDomainUrl, consumerKey, consumerSecret) {
        return new Promise((resolve, reject) => {
            const postData = querystring.stringify({
                grant_type: 'client_credentials',
                client_id: consumerKey,
                client_secret: consumerSecret,
            });

            const options = {
                hostname: myDomainUrl,
                path: '/services/oauth2/token',
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Content-Length': postData.length,
                },
            };

            const req = https.request(options, (res) => {
                let data = '';
                res.on('data', (chunk) => {
                    data += chunk;
                });
                res.on('end', () => {
                    try {
                        const response = JSON.parse(data);
                        resolve(response.access_token);
                    } catch (parseError) {
                        reject(parseError);
                    }
                });
            });

            req.on('error', (error) => {
                reject(error);
            });

            req.write(postData);
            req.end();
        });
    }

    async function createSession(agentId, accessToken, myDomainUrl) {
        return new Promise((resolve, reject) => {
            const salesforceApiUrl = 'api.salesforce.com';
            const options = {
                hostname: salesforceApiUrl,
                path: `/einstein/ai-agent/v1/agents/${agentId}/sessions`,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${accessToken}`,
                },
            };

            const req = https.request(options, (res) => {
                let data = '';
                res.on('data', (chunk) => {
                    data += chunk;
                });
                res.on('end', () => {
                    try {
                        resolve(JSON.parse(data));
                    } catch (parseError) {
                        reject(parseError);
                    }
                });
            });

            req.on('error', (error) => {
                reject(error);
            });

            const postData = JSON.stringify({
                externalSessionKey: uuidv4(),
                instanceConfig: {
                    endpoint: `https://${myDomainUrl}`,
                },
                streamingCapabilities: {
                    chunkTypes: ['Text'],
                },
                bypassUser: true,
            });

            req.write(postData);
            req.end();
        });
    }
};
```

## Agent APIでメッセージを送信するLambda


```agentforce-send-message.js
import https from 'https';
import { v4 as uuidv4 } from 'uuid';
import querystring from 'querystring';

export const handler = async (event) => {
    const agentId = process.env.AGENT_ID;
    const myDomainUrl = process.env.MY_DOMAIN_URL;
    const consumerKey = process.env.CONSUMER_KEY;
    const consumerSecret = process.env.CONSUMER_SECRET;
    const salesforceApiUrl = 'api.salesforce.com';
    const sessionId = event.sessionState.sessionAttributes.SessionId;
    const accessToken = event.sessionState.sessionAttributes.AccessToken;
    let sequenceId = Date.now();

    try {
        const messageResponse = await sendMessage(sessionId, accessToken, event.inputTranscript, sequenceId);
        console.log('Message Sent:', messageResponse);

        if (messageResponse.messages && messageResponse.messages.length > 0) {
            console.log("Response Message:", messageResponse.messages[0].message);
            if (messageResponse.messages[0].result && messageResponse.messages[0].result.length > 0) {
                console.log("Result:", JSON.stringify(messageResponse.messages[0].result, null, 2));
            }
        }

        return {
            'messages': [{'contentType': 'PlainText', 'content': messageResponse.messages[0].message}],
            'sessionState': {
                'dialogAction': {
                    'type': 'ConfirmIntent',
                },
                'intent': {
                    'name': event.sessionState.intent.name,
                    'slots': event.sessionState.intent.slots,
                    'state': 'Fulfilled'
                }
            }
        };
    } catch (error) {
        console.error('Error:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: error.message }),
        };
    }

    async function sendMessage(sessionId, accessToken, messageText, sequenceId) {
        return new Promise((resolve, reject) => {
            const options = {
                hostname: salesforceApiUrl,
                path: `/einstein/ai-agent/v1/sessions/${sessionId}/messages`,
                method: 'POST',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${accessToken}`,
                },
            };

            const req = https.request(options, (res) => {
                let data = '';
                res.on('data', (chunk) => {
                    data += chunk;
                });
                res.on('end', () => {
                    try {
                        resolve(JSON.parse(data));
                    } catch (parseError) {
                        reject(parseError);
                    }
                });
            });

            req.on('error', (error) => {
                reject(error);
            });

            const postData = JSON.stringify({
                message: {
                    sequenceId: sequenceId,
                    type: 'Text',
                    text: messageText,
                },
            });

            req.write(postData);
            req.end();
        });
    }
};
```

## Amazon Connect コンタクトフローの設定

全体の流れは以下のようになります。

![スクリーンショット 2025-03-13 18.33.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/5590e1c2-b648-45f2-b9c8-aec8cb389a26.png)

### AWS Lambdaを呼び出す

Agent APIのセッション作成を行う `agentforce-create-session` を呼び出します。

### 顧客の入力を取得する

作成したAmazon Lexを設定します。

![スクリーンショット 2025-03-13 18.35.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/e3cc9f9f-3942-4dee-8e68-d014e2bcc99e.png)

インテントには作成した `free-input` を設定します。

![スクリーンショット 2025-03-13 18.35.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/45d196b2-89d5-4ce7-9975-b16620a7f7d1.png)

`agentforce-send-message` で使用するため、`agentforce-create-session` で作成をした `sessionId` と `accessToken` をセッション属性に設定します。

![スクリーンショット 2025-03-13 18.35.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/0eddf1fc-d791-4c9f-b79a-8ec468734bbb.png)

### プロンプトの再生

Amazon Lexのスロット `freeformslot` に入ってきたテキストを読み上げます。
ここで読み上げられる文章が、Agentforceが顧客の入力を聞いた上で作成した文章となります。

![スクリーンショット 2025-03-13 18.37.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3718267/f73a699b-6f8d-46bc-8f64-50422f6666b5.png)

## 結果

期待通りに注文内容を回答してくれます。
ただし、Agentforceは若干アクション完了までのスピードが遅いため、顧客を待たせる時間が発生します。