47
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Livesense(その2)Advent Calendar 2015

Day 10

晩ごはんの献立を一緒に考えてくれるbotを作ってみた

Posted at

はじめに

この記事はリブセンスアドベントカレンダー2015(その2)の10日目です。
10日目担当の @nwatanabe です。普段は、治療ノートという医療情報系サービスのエンジニアとして、コード書いたり施策出したりと色々やってます。
突然ですが、みなさんはちゃんとご飯を作っていますか?良いコードを書くためには不健康ではダメですよね。

自分は今まで、朝食抜きの2食で生活していたのですが、そろそろいい年になってきたので、健康のために3食ちゃんとご飯を作るようにしました。ただ、1ヶ月ぐらいやってみて、いくつか問題が出てきました

  • 夕飯の献立が定型化してきている(好きなもの/思いつくものしか作らない
  • 通常、夫婦間で献立の話をする際にコミュニケーションツールとして使っているLINEでレシピ情報を展開したり、引っ張ってきたりできないので、都度外部サービスで調べるのが面倒
  • そもそも毎日新しい献立を積極的に探すほどのモチベーションはない

このままだと○○の塩焼きと○○の野菜炒め(かろうじて○○に入る食材は違う)のローテーションが続いてしまうので、そんな献立を大きく変えてくれるようなbotが欲しい、、ということで作ってみることにしました

やったこと

食べたいものがあってもなくても夕飯の献立を提案してくれるようなbotを、今回はslack+heroku+ruboty+楽天レシピAPIを利用して作りました

1. 楽天レシピAPIを登録

ざっと調べた感じでは、レシピAPIを提供しているのは楽天ウェブサービスだけだったので楽天ウェブサービス: 新規アプリ登録から登録して使わせていただきました。登録後のアプリケーションIDは後ほどherokuのconfigに設定します。

2. slackにbot用アカウントを作成する

botには、会話の流れを読んで勝手に割り込んできてほしくなかったため、RubotyをHerokuにデプロイしてSlackで使う辺りを参考に、slackでGatewayの設定(Team Settings > Permissions > Gateways から「Enable XMPP gateway (SSL only)」を選択)をしてbot用のアカウントを用意しました

3. botを用意

今回はrubyベースのrubotyを利用しました。rubotyは以前から利用しており、今利用しているslackに別のbotを入れるよりはruboty のプラグインを追加するほうがいいかなぁぐらいの理由です

Gemfile
source "https://rubygems.org"
ruby '2.2.3'
gem "ruboty"
gem "ruboty-slack"
...
Procfile
bot: bundle exec ruboty

bundle exec rubotyとかでローカル環境でも動作させることができるので、そこらへんを試したあとherokuにデプロイします

4. herokuにデプロイ

herokuへのデプロイ方法は他の記事等々でも良く紹介されているので割愛します。
成功するとこんな感じ
image

デプロイ後、1.で取得したアプリケーションIDもheroku側に設定しておきます

$ heroku config:set APPLICATION_ID=XXXXXXXXXXXXX --app=bot-application-name

ここまできたら準備完了。早速本題のものを開発します

5. 何を食べたいのか伝える

食べたいものを伝える上で、以下の処理を考えました

  • 該当するものがある場合、ランダムでレシピを返す

image

  • 該当するものがない場合、聞き返す

image

  • (実は一番個人的にニーズがある)なんでもいい場合は、勝手に選んでくれる

image

コードだとこんな感じです。
プラグインの作り方はRuboty の Plugin のつくりかた #rubotyを参考にしてます
(ざっくり作ってしまっているのでコードが汚いのは目をつぶっていただけると、、

lib/ruboty/handlers/bangohan/recommend_recipe.rb
require "ruboty/bangohan/actions/recommend_recipe"

module Ruboty
  module Handlers
    class Bangohan < Base
      on /(.*)?今日は(?<food>.+)かな\z/, name: 'recommend_recipe', description: 'output recommend recipe'

      def recommend_recipe(message)
        food = message[:food]
        Ruboty::Bangohan::Actions::RecommendRecipe.new(message).call(food)
      end
    end
  end
end
lib/ruboty/bangohan/actions/recommend_recipe.rb
require 'faraday'
require 'json'

module Ruboty
  module Bangohan
    module Actions
      class RecommendRecipe < Ruboty::Actions::Base
        RECIPE_CATEGORY_URL = "/services/api/Recipe/CategoryList/20121121?format=json&elements=categoryName%2CcategoryId%2CparentCategoryId&categoryType=medium&applicationId=#{ENV['APPLICATION_ID']}"
        RECIPES_URL = "/services/api/Recipe/CategoryRanking/20121121?format=json&formatVersion=2&applicationId=#{ENV['APPLICATION_ID']}&categoryId="

        def call(food)
          message.reply(hearing(food))
        end

        private

        def hearing(food)
          results = recipe_categories
          if food == 'なんでもいい'
            recipe_category_id = results[results.keys.sample(1)]
            recipe = choose_recipe(recipe_category_id)
            "#{food}なら、#{recipe['recipeTitle']} とかはどう?\n #{recipe['recipeUrl']}\n" +
            ' <http://webservice.rakuten.co.jp/|Supported by 楽天ウェブサービス> '
          else
            recipe_category_id = results.fetch(food, nil)
            if recipe_category_id.nil?
              "#{food}だとわからないからもうちょっと詳しく教えて"
            else
              recipe = choose_recipe(recipe_category_id)
              "#{food}だと、#{recipe['recipeTitle']} とかはどう?\n #{recipe['recipeUrl']}\n" +
              ' <http://webservice.rakuten.co.jp/|Supported by 楽天ウェブサービス> '
            end
          end
        end

        def recipe_service_api(api_path)
          @conn ||= Faraday.new(:url => 'https://app.rakuten.co.jp') do |faraday|
            faraday.request  :url_encoded
            faraday.adapter  Faraday.default_adapter
          end
        end

        def recipe_categories
          response = recipe_service_api.get(RECIPE_CATEGORY_URL)
          results = JSON.parse(response.body.force_encoding('UTF-8'))
          results["result"]["medium"].map{|result| [result['categoryName'], "#{result['parentCategoryId']}-#{result['categoryId']}"]}.to_h
        end

        def choose_recipe(recipe_category_id)
          response = recipe_service_api.get("#{RECIPES_URL}#{recipe_category_id}")
          results = JSON.parse(response.body.force_encoding('UTF-8'))
          results['result'][Random.rand(1 .. results['result'].count)]
        end
      end
    end
  end
end
ちなみに

開発時には、以下のようにgemのパスをローカル環境の開発中のものにしてheroku localでbotを立ち上げてslackで確認することができたので、いちいちherokuにデプロイしなくても動作検証できて楽でした。

Gemfile
...
gem 'ruboty-bangohan', path: '../ruboty-bangohan'
...

ここまでできたら作成したgemを含めたbotのソースをherokuにデプロイしてあげます
(今回のやつはRubyGemsにはアップせずに個人のgithubにアップして動作確認をしました

使ってみる

うまくr7kamura/ruboty-talkあたりと合わせて利用するとbotの会話も入り乱れながら普段の夫婦の晩ごはんの献立決めがこんなに賑やかになりました

image

雑感

  • rubotyプラグインはいくつか作っているけど、簡単に作れるしローカル環境でもデバッグできるので作っていて楽しい
  • 今までレパートリーになかったレシピも作るようになったし、コミュニケーションも活性化したので良かった
  • ただ一番大事なのは会社から帰ってからでもご飯を作ろうとする強い意志

本当は提案されたものが食べたい気分じゃない時に、似たような別の献立をオススメしたり、献立決めたら買い物に行くために共用のカレンダーに記載したりする機能も作っていたんですが、途中で力尽きてしまったので、それは年末までの宿題としようかと思います。ではー

47
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?