一歩先行くRPAインテグレーション実践講座 2

サンプルケース2Slack BOTとの対話、ロボットからSlackへの通知

サンプルケース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 → チャットボット

項目区分説明
ユーザIDSlack情報SlackのユーザID
顧客名見積書情報見積書に記載される顧客名
製品名見積書情報見積書に記載される製品名
数量見積書情報見積書に記載される数量

チャットボット → Orchestrator → ロボット(RPA)

項目区分説明
ユーザIDSlack情報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同様にサクッと開発してしまう点です。

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実行してもかまいません。

  1. Orchestratorとの認証
  2. ロボット一覧取得
  3. プロセス一覧取得(Release ID)
  4. 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のコードで、一連の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を受け取ります。処理の流れは次の通りです。

  1. 請求書用データ取得
  2. 請求書作成
  3. Slack返答
  4. 後処理 - 不要ファイルの削除

「引数」は以下の項目としています。

名前方向サンプル値説明
clientName入力String“ABC Inc.”見積書の会社名
productName入力String“sample”見積書の製品名
quantity入力Int3210見積書の数量
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のエンドポイントを指定します。

https://slack.com/api/files.upload

「メソッド」(HTTP Method)には、「POST」メソッドを指定します。

「オプション」

「パラメータ」(GETメソッドの引数)へは、以下の項目を指定します。

名前方向サンプル値 / 変数説明
token入力String“xoxb-5634373235-565512315213-6dksjqvZKKvXXtGuv4muXX99”Slack API登録時に生成されたToken。左はダミーのもの
channel入力String“thinkit”Slackのチャンネル。チャンネルは事前に作成しておきます
title入力Stringtitle投稿するメッセージ
filename入力StringquotationPdfファイル名
initial_comment入力Stringcommentファイルアップロード時のコメント

「添付ファイル」には、生成した請求書のPDFパスquotationPdfを指定します。

検証

すべての実装が完了したので、Rubyで作成したSlack BOTはプログラムを起動し、ワークフローはOrchestratorへデプロイしてロボットも稼働させておきます。Slackにて、一連の会話をチャットボットと行い、請求書がチャットボットからSlackで送られることを確認しましょう。

Slack BOTとの会話の中で、伝える情報が間違った場合、clearと入力することで、Slack BOTに蓄積されたデータをクリアできます。

サンプルのダウンロード

ここで使用したサンプルは、以下からダウンロードできます。

まとめ

今回のサンプル作成を通して、サンプルケース1「ロボットで実行・処理した結果を、チャットアプリへ連携し通知するもの」については、チャットアプリとの連携は、APIを使用することで簡単かつあっという間に実現できるとわかりました。

次にサンプルケース2「チャットボットとの対話にて入力されたデータをもとにRPAで自動化処理し、その結果をチャットボットで通知するもの」では、チャットアプリやRPAの既存のテクノロジーを組み合わせることで、ユーザとの対話も人手を介さずに自動化することが可能であるということがわかりました。

なお、どのような顧客、製品、数量で見積書が作成されたかをまとめて確認したいという場合、Orchestratorにすべての情報が永続的に格納されています。こちらはOrchestratorでのJOB実行の機能を利用したことで、特段そのためにロジックを作り込む必要はありません。ぜひ、チャットアプリとRPAの連携にチャレンジして、業務の自動化を促進してください。

この記事のキーワード

この記事をシェアしてください

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored