AWS Step FunctionsでConnectの通話記録をBacklogに自動起票する際、課題の担当者を対応エージェントに設定する
はじめに
以前、Amazon Connectで受けた問い合わせを自動的にBacklogのチケットとして起票する仕組みを ブログにしました。2つ目の記事では、問い合わせレコードの重複配信対策方法をブログにしました。
- Amazon Connectの通話記録をStep Functionsで処理し、Backlogに自動起票させてみた
- Amazon Connectの通話記録をKinesis Data Streamsに配信時、Step Functionsでデータの重複対策を実装する
今回は、電話対応した際に自動起票されるチケットの担当者を、AWS Step Functionsで実際に対応したエージェントに設定する方法をご紹介します。
担当者を実際に対応したエージェントにすることで、自動起票されたチケットにすぐ気づき、対応履歴を即座に記載できるため、チケット管理がしやすくなります。
なお、本記事は前回の記事で構築したリソースを前提としています。システムの全体構成は以下の通りです。
ちなみに、Backlog APIでは、description内でメンションを指定できません。
メンションによる通知を行いたい場合は、担当者としてユーザーを指定する(assigneeId)、またはコメントで通知したいユーザーを指定する(notifiedUserId)方法を取ります。
Connectユーザーにタグを設定する
Amazon Connectのユーザーに、Backlogのユーザー名と一致するタグを設定します。このタグは後ほどStep Functionsのワークフロー内で参照され、Backlogユーザーを特定するために使用されます。
- タグキー:backlog_user_name
- タグ値:Backlogのユーザー名と完全に一致する値
ステートマシン修正
前回実装したステートマシンは、データの受信から処理、Backlogチケットの作成、チケットの重複作成対策までを行う構成でした。
処理の流れは次のようになります。
- Amazon Connectで顧客との通話が行われる
- エージェントがアフターコールワークを完了すると、問い合わせレコードがAmazon Kinesis Data Streams(以下、KDS)にストリーミングされる
- EventBridge Pipesがそのデータを検知し、Step Functionsのステートマシンを起動
- Step Functionsが問い合わせデータを整形し、Backlog APIを呼び出して新規チケットを自動作成
今回は、この処理フローに以下の機能を追加します。
- KDS経由で受け取った情報から対応エージェントの情報を取得
- Connect DescribeUser APIを使用してエージェントの
backlog_user_name
タグの値を取得 - Backlogプロジェクトユーザー一覧APIを呼び出し、
backlog_user_name
の値と一致するBacklogユーザーのIDを特定 - 課題の追加後、課題を更新するAPIを呼び出し、取得したユーザーIDを使って課題の担当者を設定
これにより、対応したエージェントが自動的にBacklog課題の担当者となります。
ステートマシン定義
以下がJSONata形式で記述した完全なステートマシン定義です。各環境に合わせてプレースホルダーを置き換えて使用してください。
{
"Comment": "通話記録内容をBacklogチケットに起票する",
"StartAt": "Base64Decode",
"States": {
"Base64Decode": {
"Type": "Pass",
"Next": "ChannelTypeCheck",
"Assign": {
"decodedData": "{% $base64decode($states.input.data) %}"
}
},
"ChannelTypeCheck": {
"Type": "Choice",
"Choices": [
{
"Next": "RecordContactInDynamoDB",
"Condition": "{% $parse($decodedData).Channel = \"VOICE\" %}",
"Assign": {
"data": "{% $parse($decodedData) %}"
}
}
],
"Default": "NonVoiceChannelEnd"
},
"RecordContactInDynamoDB": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Arguments": {
"TableName": "cm-hirai-connect-backlog-tracker",
"Item": {
"contact_id": {
"S": "{% $data.ContactId %}"
},
"ttl": {
"N": "{% $string($floor($toMillis($now()) / 1000 + 86400)) %}"
}
},
"ConditionExpression": "attribute_not_exists(contact_id)"
},
"Next": "ProcessContactInParallel",
"Catch": [
{
"ErrorEquals": [
"DynamoDB.ConditionalCheckFailedException"
],
"Next": "DuplicateContactEnd",
"Comment": "既に同じコンタクトIDが存在する場合は重複処理と判断して終了する"
},
{
"ErrorEquals": [
"States.ALL"
],
"Next": "Fail",
"Comment": "その他のエラーが発生した場合は処理を失敗させる"
}
]
},
"DuplicateContactEnd": {
"Type": "Succeed",
"Comment": "既に処理済みのコンタクトIDなので処理を終了"
},
"ProcessContactInParallel": {
"Type": "Parallel",
"Next": "CreateBacklogTicket",
"Branches": [
{
"StartAt": "InquiryTypeCheck",
"States": {
"InquiryTypeCheck": {
"Type": "Choice",
"Choices": [
{
"Next": "ExtractContactData",
"Condition": "{% $data.Attributes.inquiry_type = \"product\" %}",
"Assign": {
"categoryId": "<categoryId_product>"
}
},
{
"Next": "ExtractContactData",
"Condition": "{% $data.Attributes.inquiry_type = \"contract\" %}",
"Assign": {
"categoryId": "<categoryId_contract>"
}
}
],
"Default": "ExtractContactData",
"Assign": {
"categoryId": "<categoryId_other>"
}
},
"ExtractContactData": {
"Type": "Pass",
"Assign": {
"InitialContactId": "{% $data.ContactId %}",
"InitiationTimestamp": "{% $data.InitiationTimestamp %}",
"DisconnectTimestamp": "{% $data.DisconnectTimestamp %}",
"CallerPhoneNumber": "{% $data.CustomerEndpoint.Address %}"
},
"Next": "FormatContactData"
},
"FormatContactData": {
"Type": "Pass",
"End": true,
"Output": {
"FormattedInitiationTimestamp": "{% $fromMillis($toMillis($InitiationTimestamp), '[Y0001]年[M]月[D]日 [H]時[m01]分[s01]秒', '+0900') %}",
"FormattedDisconnectTimestamp": "{% $fromMillis($toMillis($DisconnectTimestamp), '[Y0001]年[M]月[D]日 [H]時[m01]分[s01]秒', '+0900') %}",
"FormattedPhoneNumber": "{% $CallerPhoneNumber = 'anonymous' ? '非通知' : ($substring($CallerPhoneNumber, 0, 3) = '+81' ? '0' & $substring($CallerPhoneNumber, 3) : $CallerPhoneNumber) %}",
"ContactDetailsURL": "{% 'https://<Connectドメイン>/connect/contact-trace-records/details/' & $InitialContactId & '?tz=Asia/Tokyo' %}",
"categoryId": "{% $categoryId %}",
"InitialContactId": "{% $InitialContactId %}"
}
}
}
},
{
"StartAt": "CheckAgentExists",
"States": {
"CheckAgentExists": {
"Type": "Choice",
"Choices": [
{
"Next": "FinalizeAgentInfo",
"Condition": "{% $data.Agent = null %}",
"Assign": {
"connectUserTagName": "対応者はいません",
"matchedBacklogUserId": "false"
}
}
],
"Default": "RetrieveAgentInfo"
},
"RetrieveAgentInfo": {
"Type": "Task",
"Arguments": {
"InstanceId": "{% $data.Tags.\"aws:connect:instanceId\" %}",
"UserId": "{% $substringAfter($data.Agent.ARN, '/agent/') %}"
},
"Resource": "arn:aws:states:::aws-sdk:connect:describeUser",
"Assign": {
"connectUserTagName": "{% $states.result.User.Tags.backlog_user_name %}"
},
"Next": "FetchBacklogUsers",
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"Next": "FinalizeAgentInfo",
"Assign": {
"connectUserTagName": "対応者不明",
"matchedBacklogUserId": "false"
},
"Comment": "対応したエージェントにconnectUserTagName名が存在しない"
}
]
},
"FetchBacklogUsers": {
"Type": "Task",
"Resource": "arn:aws:states:::http:invoke",
"Arguments": {
"ApiEndpoint": "https://xxx.backlog.jp/api/v2/projects/<projectId>/users",
"InvocationConfig": {
"ConnectionArn": "<ContactDetailsURL>"
},
"Headers": {
"Content-Type": "application/json;charset=utf-8"
},
"Transform": {
"RequestBodyEncoding": "URL_ENCODED",
"RequestEncodingOptions": {
"ArrayFormat": "INDICES"
}
},
"Method": "GET"
},
"Retry": [
{
"ErrorEquals": [
"States.ALL"
],
"BackoffRate": 2,
"IntervalSeconds": 1,
"MaxAttempts": 3,
"JitterStrategy": "FULL"
}
],
"Catch": [
{
"ErrorEquals": [
"States.QueryEvaluationError"
],
"Comment": "connectUserTagName名とBacklogユーザー名が一致しない",
"Next": "FinalizeAgentInfo",
"Assign": {
"connectUserTagName": "対応者不明",
"matchedBacklogUserId": "false"
}
}
],
"Next": "FinalizeAgentInfo",
"Assign": {
"backlogUserList": "{% $states.result.ResponseBody %}",
"matchedBacklogUserInfo": "{% $states.result.ResponseBody[name = $connectUserTagName] %}",
"matchedBacklogUserId": "{% $states.result.ResponseBody[name = $connectUserTagName].id %}",
"connectUserTagName": "{% $connectUserTagName %}"
}
},
"FinalizeAgentInfo": {
"Type": "Pass",
"End": true,
"Output": {
"matchedBacklogUserId": "{% $matchedBacklogUserId %}",
"connectUserTagName": "{% $connectUserTagName %}"
}
}
}
}
],
"Assign": {
"FormattedInitiationTimestamp": "{% $states.result[0].FormattedInitiationTimestamp %}",
"FormattedDisconnectTimestamp": "{% $states.result[0].FormattedDisconnectTimestamp %}",
"FormattedPhoneNumber": "{% $states.result[0].FormattedPhoneNumber %}",
"InitialContactId": "{% $states.result[0].InitialContactId %}",
"ContactDetailsURL": "{% $states.result[0].ContactDetailsURL %}",
"categoryId": "{% $states.result[0].categoryId %}",
"connectUserTagName": "{% $states.result[1].connectUserTagName %}",
"matchedBacklogUserId": "{% $states.result[1].matchedBacklogUserId %}"
}
},
"NonVoiceChannelEnd": {
"Type": "Succeed"
},
"CreateBacklogTicket": {
"Type": "Task",
"Resource": "arn:aws:states:::http:invoke",
"Arguments": {
"ApiEndpoint": "https://xxx.backlog.jp/api/v2/issues",
"InvocationConfig": {
"ConnectionArn": "<ContactDetailsURL>"
},
"Headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"Transform": {
"RequestBodyEncoding": "URL_ENCODED",
"RequestEncodingOptions": {
"ArrayFormat": "INDICES"
}
},
"Method": "POST",
"RequestBody": {
"projectId": "<projectId>",
"summary": "通話記録",
"description": "{% '|項目|内容|備考|\n|---|---|---|\n|通話開始時間|' & $FormattedInitiationTimestamp & '| |\n|通話終了時間|' & $FormattedDisconnectTimestamp & '| |\n|コンタクトID|' & $InitialContactId & '| [コンタクト詳細URL](' & $ContactDetailsURL & ')|\n|対応者|' & $connectUserTagName & '| |\n|発信者電話番号|' & $FormattedPhoneNumber & '| |' %}",
"issueTypeId": "<issueTypeId>",
"categoryId[]": "{% $categoryId %}",
"priorityId": "3"
}
},
"Next": "UpdateDynamoDBWithBacklogTicketId",
"Retry": [
{
"ErrorEquals": [
"States.ALL"
],
"BackoffRate": 2,
"IntervalSeconds": 1,
"MaxAttempts": 3,
"JitterStrategy": "FULL"
}
],
"Assign": {
"backlogIssueKey": "{% $string($states.result.ResponseBody.issueKey) %}"
}
},
"UpdateDynamoDBWithBacklogTicketId": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:updateItem",
"Arguments": {
"TableName": "cm-hirai-connect-backlog-tracker",
"Key": {
"contact_id": {
"S": "{% $data.ContactId %}"
}
},
"UpdateExpression": "SET backlog_issue_id = :backlogIssueKey",
"ExpressionAttributeValues": {
":backlogIssueKey": {
"S": "{% $backlogIssueKey %}"
}
}
},
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"Next": "Fail"
}
],
"Next": "CheckAssigneeRequired"
},
"CheckAssigneeRequired": {
"Type": "Choice",
"Choices": [
{
"Next": "成功",
"Condition": "{% $matchedBacklogUserId = \"false\" %}",
"Comment": "対応者不明のため、課題の担当者は設定しない"
}
],
"Default": "AssignBacklogIssue"
},
"AssignBacklogIssue": {
"Type": "Task",
"Resource": "arn:aws:states:::http:invoke",
"Arguments": {
"ApiEndpoint": "{% 'https://xxx.backlog.jp/api/v2/issues/' & $issueId %}",
"InvocationConfig": {
"ConnectionArn": "<ContactDetailsURL>"
},
"Headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"Transform": {
"RequestBodyEncoding": "URL_ENCODED",
"RequestEncodingOptions": {
"ArrayFormat": "INDICES"
}
},
"RequestBody": {
"assigneeId": "{% $matchedBacklogUserId %}"
},
"Method": "PATCH"
},
"Retry": [
{
"ErrorEquals": [
"States.ALL"
],
"BackoffRate": 2,
"IntervalSeconds": 1,
"MaxAttempts": 3,
"JitterStrategy": "FULL"
}
],
"Next": "成功"
},
"成功": {
"Type": "Succeed"
},
"Fail": {
"Type": "Fail"
}
},
"QueryLanguage": "JSONata"
}
以下のプレースホルダーを実際の値に置き換えてください
<projectId>
:BacklogのプロジェクトID<xxx.backlog.jp>
:Backlogドメイン<issueTypeId>
:Backlog「電話」種別のID<categoryId_product>
:Backlog「商品関連」カテゴリーのID<categoryId_contract>
:Backlog「契約関連」カテゴリーのID<categoryId_other>
:Backlog「その他」カテゴリーのID<Connectドメイン>
:Amazon ConnectインスタンスのURL<ConnectionArn>
:作成したEventBridge ConnectionのARN
実行時には、Connectのユーザーにタグにより、以下のような条件分岐で対応者情報が処理されます。
- Connectのユーザーにタグが設定されていない場合:Backlogの対応者欄に「対応者不明」と表示され、担当者は設定されません
- Connectのユーザーのタグ値がBacklogのユーザー名と一致しない場合:同じく「対応者不明」と表示され、担当者は設定されません
- エージェントが対応する前に通話が終了した場合:「対応者はいません」と表示され、担当者は設定されません
いずれの場合も、Backlogチケットの担当者は設定されません。
取得可能なおおよそのユーザー数
Step Functionsは、最大256KBのペイロードサイズをサポートしています。この制限を超えると、データ処理時にエラーが発生します。
プロジェクトユーザー一覧からユーザー情報を取得する際、ユーザー数が多いと256KBの制限を超える可能性があります。制限を超えた場合、以下のようなエラーが発生します。
{
"cause": "The state/task 'http' returned a result with a size exceeding the maximum number of bytes service limit.",
"error": "States.DataLimitExceeded",
"resource": "invoke",
"resourceType": "http"
}
実際のテストでは、ユーザー一覧APIを使用した場合、約3,000ユーザーで1.4MB弱のデータサイズとなりました。
このことから推測すると、約500ユーザー程度であれば256KBの制限内に収まる可能性が高いです。
あくまで目安であり、ユーザーデータの内容によって実際のサイズは変動します。
ステートマシンのIAMポリシー
ステートマシンが必要なAPIを呼び出せるよう、適切なIAMポリシーを設定する必要があります。今回は以下の2つの権限が必要です。
- HTTP APIの呼び出し権限:Backlog APIを呼び出すために必要です。課題更新APIではPATCHメソッドを使用するため、既存のポリシーにPATCHメソッドを追加します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Sid": "InvokeHttpEndpoint1",
"Action": [
"states:InvokeHTTPEndpoint"
],
"Resource": [
"arn:aws:states:ap-northeast-1:111111111111:stateMachine:*"
],
"Condition": {
"StringEquals": {
"states:HTTPMethod": [
"POST",
"GET",
"PATCH"
]
}
}
}
]
}
- Connect API呼び出し権限:エージェント情報を取得するために、Amazon Connect APIへのアクセス権限が必要です。このため、ステートマシンの実行ロールに
AmazonConnectReadOnlyAccess
マネージドポリシーも追加で付与しました。
これらの権限を設定することで、ステートマシンは必要なすべてのAPIを呼び出すことができます。
動作確認
実際にシステムを動作させて、各ケースでの挙動を確認しました。
ケース1: 正常に担当者が設定される場合
電話対応後、エージェントがアフターコールワークを完了すると、Backlogにチケットが自動起票され、対応したエージェントが担当者として設定されました。
ステートマシンの実行フローでは、すべてのステートが正常に完了しています。
ケース2: タグ設定が不適切な場合
Connectのユーザーに以下のいずれかの問題がある場合、担当者設定が行われません。
-
タグ「backlog_user_name」が設定されていない場合
ステートマシンのRetrieveAgentInfo
ステートでエラーが発生し、Catchブロックが実行されます。"Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "FinalizeAgentInfo", "Assign": { "connectUserTagName": "対応者不明", "matchedBacklogUserId": "false" }, "Comment": "対応したエージェントにconnectUserTagName名が存在しない" } ]
-
タグの値がBacklogのユーザー名と一致しない場合
FetchBacklogUsers
ステートで一致するユーザーが見つからず、Catchブロックが実行されます。"Catch": [ { "ErrorEquals": ["States.QueryEvaluationError"], "Comment": "connectUserTagName名とBacklogユーザー名が一致しない", "Next": "FinalizeAgentInfo", "Assign": { "connectUserTagName": "対応者不明", "matchedBacklogUserId": "false" } } ]
いずれの場合も、Backlogチケットの対応者欄には「対応者不明」と表示され、matchedBacklogUserId
がfalse
となるため、担当者設定のステートがスキップされます。
どちらの場合もステートマシンの実行フローでは、CheckAssigneeRequired
ステートで条件分岐が行われ、担当者設定のステートがスキップされています。
タグの値がBacklogのユーザー名と一致しない場合の実行フロー
ケース3: エージェント未対応の場合
顧客が電話をかけたものの、エージェントが対応する前に通話が終了した場合、Agent
情報が存在しないため、CheckAgentExists
ステートで以下の条件分岐が実行されます。
"CheckAgentExists": {
"Type": "Choice",
"Choices": [
{
"Next": "FinalizeAgentInfo",
"Condition": "{% $data.Agent = null %}",
"Assign": {
"connectUserTagName": "対応者はいません",
"matchedBacklogUserId": "false"
}
}
],
"Default": "RetrieveAgentInfo"
}
この条件に一致すると、connectUserTagName
に「対応者はいません」が設定され、matchedBacklogUserId
がfalse
となります。その結果、Backlogチケットの対応者欄には「対応者はいません」と表示され、担当者設定のステートがスキップされます。
ステートマシンの実行フローでは、CheckAssigneeRequired
ステートで条件分岐が行われ、matchedBacklogUserId
がfalse
のため担当者設定のステートがスキップされています。