チャットアプリとRPAとの連携
サンプルケース2
Slack BOTとの対話、ロボットからSlackへの通知
以降の内容は初級レベルのプログラミングスキルが必要となります。
続いては、「チャットボットとの対話にて入力されたデータをもとにRPAで自動化処理し、その結果をチャットボットで通知するもの」を実装して行きたいと思います。
ここではユーザがSlack BOTと対話して得られた情報をもとに、社内で稼働するロボットに見積書を作成させることを目指します。製品や数量が都度変更する業務を想定し、ユーザは必要情報をSlackのBOTに対して入力し、Slack BOTとUiPathロボットの連携により、RPAで見積書を作成します。
Slack自体に対話する機能はないので、Slackの裏でユーザと対話するプログラムを用意します。その際のポイントとして、自然言語解析的な機能を入れてプログラムを複雑化させるのではなく、Slackをあくまでも請求書作成のための入力装置とする程度のものとします。利用者側に対しては若干不親切と思われるかもしれませんが、利用者は入力規則を守ることで、SlackからRPAを呼び出せる恩恵が得られます。私個人の意見ではありますが、まずはそのような入り口からチャットボットとRPAを連携させるのが良いと考えています。
具体的には、対話は次のような対話ができるものを目指します。
次に全体のアーキテクチャーを考えていくと、(1)チャットアプリがフロントエンドとなりユーザとインタラクティブに情報の入力を受け付け、(2)RPAでの処理結果を伝える機能を担い、(3)RPAがそのバックエンドとして必要な処理を実施し、結果を返す機能を担う体系が自然かと考えられます。
これはWebアプリケーション開発の現在のトレンドとして、フロントエンドをJavaScriptにてSPA(Single Page Application)で実装し、バックエンドをJavaのSprintBootといったフレームワークで実装して、その両者をREST APIで連携させるものと同じ構成です。
細かく考えると、次の一連のプロセスが、正当な順序ではないかと考えられます。
上図のプロセス(6)~(8)については、下図のようにロボットからSlack APIを直接呼び出すことも可能です。それにより、実装のステップも削減されます。
今回はこの簡略化したアーキテクチャーで実装を進めたいと思います。
ノード間のインターフェイス
Slack、チャットボット、Orchestrator、そしてロボット、各ノードの主要なインターフェイスに関して、こちらに整理しました。
Slack → チャットボット
項目 | 区分 | 説明 |
---|---|---|
ユーザID | Slack情報 | SlackのユーザID |
顧客名 | 見積書情報 | 見積書に記載される顧客名 |
製品名 | 見積書情報 | 見積書に記載される製品名 |
数量 | 見積書情報 | 見積書に記載される数量 |
チャットボット → Orchestrator → ロボット(RPA)
項目 | 区分 | 説明 |
---|---|---|
ユーザID | Slack情報 | SlackのユーザID |
トークン | Slack情報 | トークンの情報。ロボットに持たせておくことも可能 |
顧客名 | 見積書情報 | 見積書に記載される顧客名 |
製品名 | 見積書情報 | 見積書に記載される製品名 |
数量 | 見積書情報 | 見積書に記載される数量 |
環境(ネットワーク)
環境面で今回一点注意すべきこととして、Slack用チャットボットからOrchestratorへのJOB実行があります。社内のネットワークでOrchestratorをホスティングしている場合、Slack用チャットボットからOrchestratorへHTTPS(Port: 443)で通信可能にする必要があります。すなわち、Orchestrator側でインバウンドでのHTTPSを許可するものとなります。
社内でOrchestratorをホスティングしている場合、Slack用チャットボットも社内のサーバで運用するかと思いますので、その場合は大きな問題とはならないかもしれません。Slack用チャットボットをインターネット上で運用している場合などに注意が必要です。なお、Slack用チャットボットとSlackは、HTTPSのWebSokcetで通信を行います。Slack用チャットボットを社内で運用したとしても、アウトバウンドでのHTTPSが許可されていれば問題ありません。
Slack用チャットボット
それでは、Slack用チャットボットの作成に取り掛かります。ここからはコーディングの知識が必要です。
私個人の考えではありますが、Windows環境での業務で単純かつ自動化できるものはWindows端末のロボットに作業を任せます。一方で、チャットボットの実装といったロジックが複雑となるものはプログラミングでサクッと開発してしまいます。チャットボットとRPAについては、現時点これがすみ分けとしてちょうど良いのではないかと考えています。
今回のサンプルでは、Ruby(Version 2以降)でslack-ruby-client
というライブラリ(Gem)を使用し、Slack BOTを作成します。こちらはSlackのReal Time Messaging API
というWebSocketでのメッセージのやり取りを実装し、簡易にSlack BOTが実装可能となるライブラリです。
Rubyは、単純に私が個人的に好きなプログラミング言語です。PythonやJavaScript(Node.js)にも同様なライブラリはあるようです。ポイントとしては、スクリプト系の言語でRPA同様にサクッと開発してしまう点です。
- slack-ruby-client(https://github.com/slack-ruby/slack-ruby-client)
Ruby導入済みの環境で、UNIX系であればターミナル、Windows系であればコマンドプロンプトで、gem install slack-ruby-client
と入力してライブラリを追加しておきます。
Slack BOTでのSlack連携
下記のコードが、このライブラリを使用してSlack連携を実現したものとなります。これはライブラリの「RealTime Client」のサンプルコードを一部加工したものです。コードの真ん中のclient.on :message
というブロック内に、ボットでの見積書作成において必要事項を取得するロジックを実装することとなります。
require 'slack-ruby-client' token = 'xoxb-5634373235-565512315213-6dksjqvZKKvXXtGuv4muXX99' # 自身のSlack Appのトークンを指定する Slack.configure do |conf| conf.token = token end Slack::RealTime::Client.config do |config| config.websocket_ping = 30 end # RTM Clientのインスタンス生成 client = Slack::RealTime::Client.new quotations = {} client.on :hello do puts "Successfully connected, welcome '#{client.self.name}' to the '#{client.team.name}' team at https://#{client.team.domain}.slack.com." end client.on :message do |data| next if client.self.id == data.user client.typing channel: data.channel quotations[data.user] = Quotation.new(data.user) unless quotations.key? data.user quotations[data.user].reply(data.text) { |reply| client.message channel: data['channel'], text: reply } end client.on :close do |_data| puts 'Connection closing, exiting.' end client.on :closed do |_data| puts 'Connection has been disconnected.' end client.start!
このコードで、Slack BOTのクライアントとして接続できます。次にclient.on :message
へ実装する対話のロジックを考えてみます。
見積のためのQuotationクラス
対話ロジックを実装するにあたり、「Quotation」というクラスを作成しました。このクラス内にユーザから取得する顧客名(client_name)、製品名(product_name)、数量(quantity)、SlackユーザID(requester)のインスタンス変数を用意します。こちらはオブジェクト指向のクラス設計で、このクラスにて1件の見積りで必要となる情報をハンドリングします。
reply(message)
というブロックのメソッドにて、Slack経由でのユーザとの対話処理を実装します。このメソッド内で、is_new
というインスタンス変数と顧客名、製品名、数量のそれぞれの取得状況に応じて、ユーザへの入力依頼をブロックで返します。なお、すべての情報が取得でき次第、start_job
という外部のメソッドでロボットへのジョブを依頼します。
要はclient.on :message
の中で、このQuotationクラスのインスタンスを使用して、reply(message)
メソッドを使用してユーザとの対話を実現するためのものとなります。
class Quotation attr_reader :client_name, :product_name, :quantity, :requester def initialize(requester) @requester = requester @is_new = true end # 対話での見積書作成用データの取得メソッド def reply(message) parsed_message = parse_message(message) if parsed_message == 'summary' yield "#{user_symbol} サマリの表示ですね" yield "情報未取得のため、表示できるサマリはありません" if @is_new elsif parsed_message == 'clear' yield "#{user_symbol} BOTで収集した情報をクリアします" clear yield "クリアしました" elsif @is_new if ['Yes', 'yes', 'はい'].include? message yield "#{user_symbol} 承知しました。それでは、、、" yield "顧客名は何? \ni.e.) ABC株式会社" @is_new = false else yield "#{user_symbol} 請求書作成BOTです。" yield "作成開始しますか? \ni.e.) #{['Yes', 'yes', 'はい'].join(', ')}" end elsif @client_name.nil? @client_name = parsed_message yield "#{user_symbol} 顧客名は *#{@client_name}* ですね" yield "製品名は何? \ni.e.) ネジ" elsif @product_name.nil? @product_name = parsed_message yield "#{user_symbol} 製品名は *#{@product_name}* ですね" yield "数量は? \ni.e.) 1000" elsif @quantity.nil? @quantity = parsed_message.to_i rescue 0 yield "#{user_symbol} 数量は *#{@quantity.to_s}* ですね" yield "UiPath Robotにて請求書を作成します!!" start_job(@requester, @client_name, @product_name, @quantity) end yield "```BOTの収集情報\n" + summary + "```" unless @is_new end # 文字列のクレンジングを実装する。 def parse_message(message) message.strip end # Slackでのユーザ表記へ変換 def user_symbol "<@#{@requester}>" end def clear @is_new = true @client_name = nil @product_name = nil @quantity = nil end def summary "顧客名: #{@client_name}\n製品名: #{@product_name}\n数量: #{@quantity}\nコマンド: summary, clear" end end
こちらのクラスを工夫することで、見積書作成においてさらに必要なデータを取得したり、ユーザとのより高度な対話を実現したりもできます。一例として、入力文字列のクレンジング処理用の`parse_message(message)`メソッドを用意していますが、このメソッドの中で自然言語処理を実装したり、必要な情報のみを抽出したりすることができます。
Orchestrator REST APIの呼び出し
それでは、説明ができていなかったstart_job
メソッドについて説明します。こちらでもREST APIの呼び出しに便利なrest-client
というライブラリを使用します。環境に未導入の場合、gem install rest-client
で導入しておきます。Orchestratorとのやり取りはJSON形式となりますので、json
のライブラリをrequireしておきます。こちらはRuby標準に付属されております。
Orchestratorに関するURLやユーザ名、パスワードといった情報は今回は定数としてあらかじめ定義しています。start_job
のメソッド内では、引数からのロボット名とプロセス名の情報をもとに、APIを使用してJOB実行まで一気通貫で実行します。こちらはOrchestratorへ接続しているロボットの状況などで、適宜呼び出し方法を変更しても問題ありません。
例えば、特定のロボットで実行する必要がない場合、JOB実行のNoOfRobots
のパラメータを使用することで、ロボット一覧取得
の呼び出しは不要となります。他にもRelease IDがわかっている場合、直接そのIDでJOB実行してもかまいません。
- Orchestratorとの認証
- ロボット一覧取得
- プロセス一覧取得(Release ID)
- JOB実行
require 'json' require 'rest-client' BASE_ENDPOINT = "https://hostname" # Orchestrator URL TENANCY_NAME = "default" # Orchestrator テナント名 EMAIL_ADDRESS = '<user id or email>' # Orchestrator ユーザ名 or email PASSWORD = '<password>' # Orchestrator パスワード OC_ENV = 'prod' # Orchestrator ロボットグループ PROCESS_NAME = "slack_quotation_thinkit" # Orchestrator プロセス名 ROBOT_NAME = "my-robot" # Orchestrator ロボット名 def start_job(slack_user, client_name, product_name, quantity, base_endpoint = BASE_ENDPOINT) # POST /api/account/authenticate - Orchestratorとの認証 res = RestClient.post( base_endpoint + "/api/account/authenticate", { "tenancyName" => TENANCY_NAME, "usernameOrEmailAddress" => EMAIL_ADDRESS, "password" => PASSWORD } ) json_result = JSON.parse(res.body) token = json_result['result'] # GET /odata/Robots - ロボット一覧取得 endpoint = base_endpoint + "/odata/Robots" res = RestClient.get endpoint, { :Authorization => "Bearer #{token}", content_type: :json, accept: :json } json_result = JSON.parse(res.body) robot = json_result["value"].find { |robot| robot['Name'] == ROBOT_NAME and robot['RobotEnvironments'] == OC_ENV } # GET /odata/Releases - プロセス一覧取得 endpoint = base_endpoint + "/odata/Releases" res = RestClient.get endpoint, { :Authorization => "Bearer #{token}", content_type: :json, accept: :json } json_result = JSON.parse(res.body) release = json_result["value"].find { |release| release['Name'] == PROCESS_NAME and release['EnvironmentName'] == OC_ENV #and release['IsLatestVersion'] == true } # POST /odata/Jobs/UiPath.Server.Configuration.OData.StartJobs - JOB実行 endpoint = base_endpoint + "/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs" payload = { "startInfo": { "ReleaseKey": release["Key"], "Strategy": "Specific", "RobotIds": [ robot["Id"].to_i ], #"NoOfRobots": 0, "Source": "Manual", "InputArguments": "{\"channels\":\"thinkit\",\"slackUserId\":\"#{slack_user}\",\"clientName\":\"#{client_name}\",\"productName\":\"#{product_name}\",\"quantity\":#{quantity}}" } }.to_json res = RestClient.post endpoint, payload, { :Authorization => "Bearer #{token}", content_type: :json, accept: :json } end
OrchestratorのAPIについては、UiPahtのWebサイトに詳細が記載されていますので、そちらも参考にしてみてください。また、OrchestratorにおいてはSwaggerも内包されていますので、SwaggerからAPIをテストすることも可能です。
このサンプルでは、プロセス一覧とロボット一覧をすべて取得しています。プロセスとロボット数の登録が多いといった場合、Orchestratorへの問い合わせの際に一般的なOData句が利用可能で、$Filterを使用して対象データを絞ることも可能です。詳細は、OrchestratorのAPIを参照してみてください。
- Orchestrator API(https://docs.uipath.com/orchestrator/lang-ja/reference)
複数ユーザとの会話
前述のOrchestratorのコードで、一連のSlack BOTの処理は完結します。最後に、Slackのチャンネル内で複数ユーザとのやり取りを円滑に実現できる仕組みを説明します。
client.on :message
のブロックの中では、Hashクラス(ディクショナリー型)を利用して、SlackユーザIDをキーに、Quotationクラスのインスタンスを保持する仕組みとしています。quotationsのHashにメッセージを受け取ったユーザが含まれていない場合、新規にQuotationクラスのインスタンスを生成してHashに格納し、Quotationクラスのreply
ブロックでユーザと対話させています。
client.on :message do |data| next if client.self.id == data.user client.typing channel: data.channel quotations[data.user] = Quotation.new(data.user) unless quotations.key? data.user quotations[data.user].reply(data.text) { |reply| client.message channel: data['channel'], text: reply } end
next if client.self.id == data.user
は、ロボットでの見積書作成後の投稿に対してSlack BOT側で反応しないようにするためのものです。
UiPathロボット
ロボットはOrchestratorからJOB実行されることを前提とし、その際に引数で請求書作成に必要なデータや、依頼者のSlackユーザIDを受け取ります。処理の流れは次の通りです。
- 請求書用データ取得
- 請求書作成
- Slack返答
- 後処理 - 不要ファイルの削除
「引数」は以下の項目としています。
名前 | 方向 | 型 | サンプル値 | 説明 |
---|---|---|---|---|
clientName | 入力 | String | “ABC Inc.” | 見積書の会社名 |
productName | 入力 | String | “sample” | 見積書の製品名 |
quantity | 入力 | Int32 | 10 | 見積書の数量 |
channels | 入力 | String | “thinkit” | Slackチャンネル名 |
slackUserId | 入力 | String | “U9N4ZXXXX” | SlackユーザID |
こちらは、Main.xamlの引数を、Orchestratorの「Input Arguments」の機能を利用して、API経由で受けります。この機能はOrchestrator ver2018.4以降で利用可能です。古いバージョンの場合はQueue機能で代替できます。
請求書用データの取得
こちらでRPAのテクノロジーを用いて、社内の各種システムから必要なデータを取得します。このサンプルでは、会社名や製品名、その製品は一つのみとしていますが、会社コードや製品コードを解決したり、実際の業務用にこちらでカスタマイズを行ったりします。
請求書作成
Wordアクティビティにて、テンプレートとなる見積書のWordファイルに、見積書で必要なデータを置き換えて行きます。データ置き換え後、最後にWordファイルをPDFに変換します。
Slackで返答
請求書のPDFが作成されましたので、SlackへPDFファイルをアップロードします。ファイルのアップロードについては、使用するSlack APIは以下となります。
- Slack API files.upload(https://api.slack.com/methods/files.upload)
「入力」
「エンドポイント」には、下記のSlack APIのエンドポイントを指定します。
https://slack.com/api/files.upload
「メソッド」(HTTP Method)には、「POST」メソッドを指定します。
「オプション」
「パラメータ」(GETメソッドの引数)へは、以下の項目を指定します。
名前 | 方向 | 型 | サンプル値 / 変数 | 説明 |
---|---|---|---|---|
token | 入力 | String | “xoxb-5634373235-565512315213-6dksjqvZKKvXXtGuv4muXX99” | Slack API登録時に生成されたToken。左はダミーのもの |
channel | 入力 | String | “thinkit” | Slackのチャンネル。チャンネルは事前に作成しておきます |
title | 入力 | String | title | 投稿するメッセージ |
filename | 入力 | String | quotationPdf | ファイル名 |
initial_comment | 入力 | String | comment | ファイルアップロード時のコメント |
「添付ファイル」には、生成した請求書のPDFパスquotationPdf
を指定します。
検証
すべての実装が完了したので、Rubyで作成したSlack BOTはプログラムを起動し、ワークフローはOrchestratorへデプロイしてロボットも稼働させておきます。Slackにて、一連の会話をチャットボットと行い、請求書がチャットボットからSlackで送られることを確認しましょう。
Slack BOTとの会話の中で、伝える情報が間違った場合、clear
と入力することで、Slack BOTに蓄積されたデータをクリアできます。
サンプルのダウンロード
ここで使用したサンプルは、以下からダウンロードできます。
まとめ
今回のサンプル作成を通して、サンプルケース1「ロボットで実行・処理した結果を、チャットアプリへ連携し通知するもの」
については、チャットアプリとの連携は、APIを使用することで簡単かつあっという間に実現できるとわかりました。
次にサンプルケース2「チャットボットとの対話にて入力されたデータをもとにRPAで自動化処理し、その結果をチャットボットで通知するもの」
では、チャットアプリやRPAの既存のテクノロジーを組み合わせることで、ユーザとの対話も人手を介さずに自動化することが可能であるということがわかりました。
なお、どのような顧客、製品、数量で見積書が作成されたかをまとめて確認したいという場合、Orchestratorにすべての情報が永続的に格納されています。こちらはOrchestratorでのJOB実行の機能を利用したことで、特段そのためにロジックを作り込む必要はありません。ぜひ、チャットアプリとRPAの連携にチャレンジして、業務の自動化を促進してください。