はじめに
LINEWORKSのBOTを利用して、Claude 3と会話してみたい!と思った人向け
以前、LINEで同様なことをしてます
今回は、LINEWORKS(企業向けLINE)の記事です
個人向けLINEで、利用する場合は、下記をご覧ください
LINEWORKSとLINEってどう違うの?
LINEWORKSとLINEの違いについて
「LINE WORKS」は、情報や予定を共有しあって活動する、組織・チームのためのコミュニケーションツール。
企業や企業内プロジェクト、特定の活動をする団体などに最適です。
LINE⇒個人向け
LINEWORKS⇒企業(組織・チーム)向け
の位置づけのようです
企業にフォーカスしているため、
アカウント管理やトークの監査機能など、組織として必要な機能が実装されています
また、掲示板機能なども実装されています
プランによっては、メール機能も使えるみたいです
LINEWORKS BOTの作成
では、さっそくLINEWORKS BOTを作成していきます
作業内容について
- LINEWORKS Developerの準備
- BOT作成
- API作成
- AWS側のリソース作成
- Lambda Layerの作成(Cloud9)
- Lambda / APIGateway作成
- APIGatewayとLINEWORKSBOTの連携
- LINEWORKSでBOT公開
- 動作確認
ざっくり、こんなかんじです
前提
- LINEWORKSのアカウントをすでに持っている
- BOT権限を管理者から付与されている
- AWSアカウントを所有している
- AWSの利用リージョンはバージニア(us-east-1)
LINEWORKS Developerの準備
下記のサイトにログインし、BOTとAPIの設定を行います。
https://developers.worksmobile.com/jp/
API作成
OAuth Scopesの設定を行います。
権限はbotのみでOKです
Client ID / Client Secretをコピーします
Service Accountの発行を行います
Service Accountの発行ができると、アカウントIDが表示されるので、コピーします
Private Keyも利用しますので、発行し、Key情報がダウンロードされるので、保存します。
APIの作成はこれで終わりです。
BOT作成
AWS側のリソース作成
Lambda Layerの作成(Cloud9)
LambdaでLINEWORKS BOTへPOSTする為、アクセストークンを取得する必要があります。
Service Account 認証 (JWT)
Lambda(Python)で、JWTを取り扱うにはLayerを追加する必要があります
Layerの作り方については、別記事を作成して公開していますので、
こちらをご覧ください
Lambda / APIGateway作成
AWS上にLambdaとAPIGatewayを作成していきます
Lambda
アーキテクチャーの指定に気を付けてください
Cloud9で作ったLayer環境と同じアーキテクチャーを指定しましょう
import json
import os
import jwt
import boto3
from datetime import datetime
import urllib
import requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
BASE_API_URL = "https://www.worksapis.com/v1.0"
BASE_AUTH_URL = "https://auth.worksmobile.com/oauth2/v2.0"
class AuthManager:
def __init__(self):
self.client_id, self.client_secret, self.service_account_id, self.private_key = self._get_env_vars()
def _get_env_vars(self):
client_id = os.environ.get("LW_API_20_CLIENT_ID")
client_secret = os.environ.get("LW_API_20_CLIENT_SECRET")
service_account_id = os.environ.get("LW_API_20_SERVICE_ACCOUNT_ID")
private_key_str = os.environ['LW_API_20_PRIVATEKEY']
private_key_str = f"-----BEGIN RSA PRIVATE KEY-----\n{private_key_str}\n-----END RSA PRIVATE KEY-----"
privatekey = load_pem_private_key(private_key_str.encode(), password=None)
return client_id, client_secret, service_account_id, privatekey
def _generate_jwt(self):
current_time = datetime.now().timestamp()
iss = self.client_id
sub = self.service_account_id
iat = current_time
exp = current_time + (60 * 60) # 1時間
jws = jwt.encode(
{
"iss": iss,
"sub": sub,
"iat": iat,
"exp": exp
}, self.private_key, algorithm="RS256")
return jws
def get_access_token(self, scope):
jws = self._generate_jwt()
url = f'{BASE_AUTH_URL}/token'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
params = {
"assertion": jws,
"grant_type": urllib.parse.quote("urn:ietf:params:oauth:grant-type:jwt-bearer"),
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": scope,
}
r = requests.post(url=url, data=params, headers=headers)
return json.loads(r.text)
def refresh_access_token(self, refresh_token):
url = f'{BASE_AUTH_URL}/token'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
params = {
"refresh_token": refresh_token,
"grant_type": "refresh_token",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
r = requests.post(url=url, data=params, headers=headers)
return json.loads(r.text)
class MessageSender:
def __init__(self, bot_id, user_id):
self.bot_id = bot_id
self.user_id = user_id
def send_message(self, content, access_token):
url = f"{BASE_API_URL}/bots/{self.bot_id}/users/{self.user_id}/messages"
headers = {
'Content-Type': 'application/json',
'Authorization': f"Bearer {access_token}"
}
form_data = json.dumps(content)
r = requests.post(url=url, data=form_data, headers=headers)
r.raise_for_status()
def lambda_handler(event, context):
try:
# メッセージを受信
if event['type'] == 'message':
# メッセージタイプがテキストの場合
if event['content']['type'] == 'text':
# 受信メッセージ
messageText = event['content']['text']
# 送信元ユーザーID
user_id = event['source']['userId']
bot_id = os.environ.get("LW_API_20_BOT_ID")
auth_manager = AuthManager()
message_sender = MessageSender(bot_id, user_id)
scope = "bot"
res = auth_manager.get_access_token(scope)
access_token = res["access_token"]
# 利用者がBOTを利用開始したタイミングの処理(postback: startが含まれているかチェック)
if "postback" in event['content'] and event['content']['postback'] == "start":
response_text = "ClaudeBOTへ問い合わせありがとうございます\nなんでも聞いてくださいね!"
# メッセージを返信(Amazon BedRock Claude 3 Sonnetからの応答の中身を返す)
content = {
"content": {
"type": "text",
"text": response_text
}
}
message_sender.send_message(content, access_token)
else:
bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
model_id = 'anthropic.claude-3-sonnet-20240229-v1:0'
max_tokens = 2000
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": messageText}]
}
)
# Amazon BedRock Claude 3 Sonnetに質問を送信
response = bedrock_runtime.invoke_model(body=body, modelId=model_id)
response_body = response['body'].read().decode('utf-8')
# Amazon BedRockからの応答の中身のみを取り出す
response_text = json.loads(response_body)['content'][0]['text']
# メッセージを返信(Amazon BedRock Claude 3 Sonnetからの応答の中身を返す)
content = {
"content": {
"type": "text",
"text": response_text
}
}
message_sender.send_message(content, access_token)
# エラーが起きた場合
except Exception as e:
print(f"エラー: {e}")
return {'statusCode': 500, 'body': json.dumps(f'Exception occurred: {str(e)}')}
return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
Layerの追加
requestsとjwt用のlayerを追加します
arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p312-requests:3
jwtのlayer作成は、こちらで完了しているはずなので、省略します
設定
一般設定
タイムアウト:10分
アクセス権限:CloudWatchLogs、Bedrockの権限
環境変数:
変数名 | 内容 |
---|---|
LW_API_20_BOT_ID | LINEWORKS Developerで登録したBOTID |
LW_API_20_CLIENT_ID | LINEWORKS Developer APIのClient ID |
LW_API_20_CLIENT_SECRET | LINEWORKS Developer APIのClient Secret |
LW_API_20_PRIVATEKEY | LINEWORKS Developer APIでダウンロードしたkeyの中身※ |
LW_API_20_SERVICE_ACCOUNT_ID | LINEWORKS Developer APIのService Account ID |
※プライベートキーは、
先頭(-----BEGIN PRIVATE KEY-----)と末尾(-----END PRIVATE KEY-----)を除いて、
LW_API_20_PRIVATEKEYに入れてください。
APIGateway
作成したLambdaを指定します
また、HTTPリクエストヘッダーとして、X-WORKS-Signatureを必須として、指定します
HTTPリクエストヘッダーの設定は、忘れずに設定ください
不要なアタックを遮断するための設定です。
APIGatewayとLINEWORKSBOTの連携
APIGateway作成後に、コピーしたURLをBOTと紐づけます
LINEWORKSでBOT公開
管理者画面へ行き、BOTを登録します
https://admin.worksmobile.com/service/bot
BOTがLINEWORKSに登録できた状態
公開設定をONにした場合、LINEWORKSユーザー全員へ通知が行きます
ご注意ください
動作確認
今回参考にさせていただいたサイト
CloudFormation
上記の内容を、CloudFormation化しました
- 前提
- バージニア(us-east-1)で展開
- JWTのLayerは手動で調整が必要
- LINEWORKS側のBOT登録作業は別途必要
- Amazon Bedrockから、Claude 3 Sonnetへリクエストが可能な状態
AWSTemplateFormatVersion: '2010-09-09'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: SYSTEMNAME
Parameters:
- NameTag
Label:
default: LINEWORKS
Parameters:
- LINEWORKSCLIENTID
- LINEWORKCLIENTSECRET
- LINEWORKSERVICEACCOUNTID
- LINEWORKPRIVATEKEY
- LINEWORKSBOTID
Parameters:
NameTag:
Type: String
Description: 'Common resource name'
Default: 'LINEWORKSBOT'
LINEWORKSBOTID:
Type: String
Description: 'ChannelAccessToken for LINEBOT'
Default: '01234567'
LINEWORKSCLIENTID:
Type: String
Description: 'Enter the client ID issued by LINEWORKS Developer'
Default: 'none'
LINEWORKCLIENTSECRET:
Type: String
Description: 'Enter the client secret issued by LINEWORKS Developer'
NoEcho: true
LINEWORKPRIVATEKEY:
Type: String
Description: 'Enter the private key issued by LINEWORKS Developer'
NoEcho: true
LINEWORKSERVICEACCOUNTID:
Type: String
Description: 'Enter the service account ID issued by LINEWORKS Developer'
Default: 'XXXX.serviceaccount@XXX'
Resources:
linebotFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub '${NameTag}-lambda'
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
ZipFile: !Sub |
import json
import os
import jwt
import boto3
from datetime import datetime
import urllib
import requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
BASE_API_URL = "https://www.worksapis.com/v1.0"
BASE_AUTH_URL = "https://auth.worksmobile.com/oauth2/v2.0"
class AuthManager:
def __init__(self):
self.client_id, self.client_secret, self.service_account_id, self.private_key = self._get_env_vars()
def _get_env_vars(self):
client_id = os.environ.get("LW_API_20_CLIENT_ID")
client_secret = os.environ.get("LW_API_20_CLIENT_SECRET")
service_account_id = os.environ.get("LW_API_20_SERVICE_ACCOUNT_ID")
private_key_str = os.environ['LW_API_20_PRIVATEKEY']
private_key_str = f"-----BEGIN RSA PRIVATE KEY-----\n{private_key_str}\n-----END RSA PRIVATE KEY-----"
privatekey = load_pem_private_key(private_key_str.encode(), password=None)
return client_id, client_secret, service_account_id, privatekey
def _generate_jwt(self):
current_time = datetime.now().timestamp()
iss = self.client_id
sub = self.service_account_id
iat = current_time
exp = current_time + (60 * 60) # 1時間
jws = jwt.encode(
{
"iss": iss,
"sub": sub,
"iat": iat,
"exp": exp
}, self.private_key, algorithm="RS256")
return jws
def get_access_token(self, scope):
jws = self._generate_jwt()
url = f'{BASE_AUTH_URL}/token'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
params = {
"assertion": jws,
"grant_type": urllib.parse.quote("urn:ietf:params:oauth:grant-type:jwt-bearer"),
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": scope,
}
r = requests.post(url=url, data=params, headers=headers)
return json.loads(r.text)
def refresh_access_token(self, refresh_token):
url = f'{BASE_AUTH_URL}/token'
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
params = {
"refresh_token": refresh_token,
"grant_type": "refresh_token",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
r = requests.post(url=url, data=params, headers=headers)
return json.loads(r.text)
class MessageSender:
def __init__(self, bot_id, user_id):
self.bot_id = bot_id
self.user_id = user_id
def send_message(self, content, access_token):
url = f"{BASE_API_URL}/bots/{self.bot_id}/users/{self.user_id}/messages"
headers = {
'Content-Type': 'application/json',
'Authorization': f"Bearer {access_token}"
}
form_data = json.dumps(content)
r = requests.post(url=url, data=form_data, headers=headers)
r.raise_for_status()
def lambda_handler(event, context):
try:
# メッセージを受信
if event['type'] == 'message':
# メッセージタイプがテキストの場合
if event['content']['type'] == 'text':
# 受信メッセージ
messageText = event['content']['text']
# 送信元ユーザーID
user_id = event['source']['userId']
bot_id = os.environ.get("LW_API_20_BOT_ID")
auth_manager = AuthManager()
message_sender = MessageSender(bot_id, user_id)
scope = "bot"
res = auth_manager.get_access_token(scope)
access_token = res["access_token"]
# 利用者がBOTを利用開始したタイミングの処理(postback: startが含まれているかチェック)
if "postback" in event['content'] and event['content']['postback'] == "start":
response_text = "ClaudeBOTへ問い合わせありがとうございます\nなんでも聞いてくださいね!"
# メッセージを返信(Amazon BedRock Claude 3 Sonnetからの応答の中身を返す)
content = {
"content": {
"type": "text",
"text": response_text
}
}
message_sender.send_message(content, access_token)
else:
bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
model_id = 'anthropic.claude-3-sonnet-20240229-v1:0'
max_tokens = 2000
body = json.dumps(
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": messageText}]
}
)
# Amazon BedRock Claude 3 Sonnetに質問を送信
response = bedrock_runtime.invoke_model(body=body, modelId=model_id)
response_body = response['body'].read().decode('utf-8')
# Amazon BedRockからの応答の中身のみを取り出す
response_text = json.loads(response_body)['content'][0]['text']
# メッセージを返信(Amazon BedRock Claude 3 Sonnetからの応答の中身を返す)
content = {
"content": {
"type": "text",
"text": response_text
}
}
message_sender.send_message(content, access_token)
# エラーが起きた場合
except Exception as e:
print(f"エラー: {e}")
return {'statusCode': 500, 'body': json.dumps(f'Exception occurred: {str(e)}')}
return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
MemorySize: 512
Timeout: 600
Architectures:
- x86_64
Environment:
Variables:
LW_API_20_BOT_ID: !Ref LINEWORKSBOTID
LW_API_20_CLIENT_ID: !Ref LINEWORKSCLIENTID
LW_API_20_CLIENT_SECRET: !Ref LINEWORKCLIENTSECRET
LW_API_20_PRIVATEKEY: !Ref LINEWORKPRIVATEKEY
LW_API_20_SERVICE_ACCOUNT_ID: !Ref LINEWORKSERVICEACCOUNTID
Layers:
- arn:aws:lambda:us-east-1:770693421928:layer:Klayers-p312-requests:3
Tags:
- Key: Name
Value: !Ref NameTag
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt linebotFunction.Arn
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/
DependsOn:
- ApiGateway
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: AmazonBedrock
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: bedrock:InvokeModel
Resource: !Sub 'arn:aws:bedrock:${AWS::Region}::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0'
Tags:
- Key: Name
Value: !Ref NameTag
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: !Sub '${NameTag}-APIGateway'
EndpointConfiguration:
Types:
- REGIONAL
Tags:
- Key: Name
Value: !Ref NameTag
DependsOn:
- linebotFunction
ApiGatewayRootMethod:
Type: AWS::ApiGateway::Method
DependsOn:
- LambdaPermission
Properties:
RestApiId: !Ref ApiGateway
ResourceId: !GetAtt ApiGateway.RootResourceId
HttpMethod: POST
AuthorizationType: NONE
RequestParameters:
method.request.header.X-WORKS-Signature: true
MethodResponses:
- StatusCode: '200'
ResponseModels:
application/json: Empty
Integration:
Type: AWS
IntegrationHttpMethod: POST
Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${linebotFunction.Arn}/invocations'
PassthroughBehavior: WHEN_NO_MATCH
ContentHandling: CONVERT_TO_TEXT
TimeoutInMillis: 29000
CacheNamespace: !GetAtt ApiGateway.RootResourceId
IntegrationResponses:
- StatusCode: '200'
ApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
DependsOn:
- ApiGatewayRootMethod
Properties:
RestApiId: !Ref ApiGateway
ApiGatewayStage:
Type: AWS::ApiGateway::Stage
DependsOn:
- ApiGatewayDeployment
Properties:
RestApiId: !Ref ApiGateway
DeploymentId: !Ref ApiGatewayDeployment
StageName: prod
Tags:
- Key: Name
Value: !Ref NameTag
さいごに
LINEWORKSのBOTを作成してみました
jwtの部分で大分苦戦しましたが、なんとか動いて良かったです
今回調べていく中、LINEWORKSの記事自体が少なく、見つけても根拠としている、
LINEWORKS Developer側のURLがリンク切れになっていることが多かった印象です
リファレンスの管理って大変だなと感じました
さて、次はRAG周りに挑戦しようと思います。
Claudeにもっとちゃんとした知識を元に回答してくれるようにしたいと思います
それでは、また!