├── souschef ├── __init__.py ├── user_state.py ├── recipe.py ├── souschef.py └── cloudant_recipe_store.py ├── Procfile ├── .cfignore ├── screenshots ├── cfapp1.png ├── cfapp2.png ├── local1.png ├── slack1.png ├── slack2.png ├── bluemix1.png ├── cloudant1.png ├── sous-chef1.png ├── conversation1.png ├── conversation2.png ├── conversation3.png ├── conversation4.png ├── conversation5.png ├── conversation6.png ├── conversation7.png ├── spoonacular1.png ├── spoonacular2.png ├── spoonacular3.png ├── sous-chef-convo1.png ├── sous-chef-convo2.png └── sous-chef-convo3.png ├── static ├── images │ └── newapp-icon.png ├── index.html └── stylesheets │ └── style.css ├── manifest.yml ├── requirements.txt ├── .env.template ├── scripts └── get_bot_id.py ├── .gitignore ├── run.py ├── server.py ├── deployment_tracker.py ├── workspace.json ├── LICENSE.txt └── README.md /souschef/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python server.py -------------------------------------------------------------------------------- /.cfignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | scripts 4 | venv 5 | workspace.json -------------------------------------------------------------------------------- /screenshots/cfapp1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/cfapp1.png -------------------------------------------------------------------------------- /screenshots/cfapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/cfapp2.png -------------------------------------------------------------------------------- /screenshots/local1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/local1.png -------------------------------------------------------------------------------- /screenshots/slack1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/slack1.png -------------------------------------------------------------------------------- /screenshots/slack2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/slack2.png -------------------------------------------------------------------------------- /screenshots/bluemix1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/bluemix1.png -------------------------------------------------------------------------------- /screenshots/cloudant1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/cloudant1.png -------------------------------------------------------------------------------- /screenshots/sous-chef1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/sous-chef1.png -------------------------------------------------------------------------------- /screenshots/conversation1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation1.png -------------------------------------------------------------------------------- /screenshots/conversation2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation2.png -------------------------------------------------------------------------------- /screenshots/conversation3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation3.png -------------------------------------------------------------------------------- /screenshots/conversation4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation4.png -------------------------------------------------------------------------------- /screenshots/conversation5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation5.png -------------------------------------------------------------------------------- /screenshots/conversation6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation6.png -------------------------------------------------------------------------------- /screenshots/conversation7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/conversation7.png -------------------------------------------------------------------------------- /screenshots/spoonacular1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/spoonacular1.png -------------------------------------------------------------------------------- /screenshots/spoonacular2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/spoonacular2.png -------------------------------------------------------------------------------- /screenshots/spoonacular3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/spoonacular3.png -------------------------------------------------------------------------------- /static/images/newapp-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/static/images/newapp-icon.png -------------------------------------------------------------------------------- /screenshots/sous-chef-convo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/sous-chef-convo1.png -------------------------------------------------------------------------------- /screenshots/sous-chef-convo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/sous-chef-convo2.png -------------------------------------------------------------------------------- /screenshots/sous-chef-convo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibm-watson-data-lab/watson-recipe-bot-python-cloudant/HEAD/screenshots/sous-chef-convo3.png -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - path: . 3 | memory: 128M 4 | instances: 1 5 | domain: mybluemix.net 6 | name: watson-recipe-bot-cloudant 7 | host: watson-recipe-bot-cloudant-${random-word} 8 | disk_quota: 1024M -------------------------------------------------------------------------------- /souschef/user_state.py: -------------------------------------------------------------------------------- 1 | class UserState(object): 2 | def __init__(self, user_id): 3 | self.user_id = user_id 4 | self.conversation_context = {} 5 | self.user = None 6 | self.ingredient_cuisine = None 7 | 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | cloudant==2.3.1 3 | ordereddict==1.1 4 | pysolr==3.5.0 5 | python-dotenv==0.5.1 6 | requests==2.11.0 7 | six==1.10.0 8 | slackclient==1.0.1 9 | watson-developer-cloud==0.23.0 10 | websocket-client==0.37.0 11 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SLACK_BOT_TOKEN= 2 | SLACK_BOT_ID= 3 | SPOONACULAR_KEY= 4 | CONVERSATION_USERNAME= 5 | CONVERSATION_PASSWORD= 6 | CONVERSATION_WORKSPACE_ID= 7 | CLOUDANT_USERNAME= 8 | CLOUDANT_PASSWORD= 9 | CLOUDANT_URL= 10 | CLOUDANT_DB_NAME=watson_recipe_bot -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Python Starter Application 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 18 | 19 |
12 | 13 | 15 |

Hello World!

16 |

Thanks for creating a Python Starter Application. 17 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /scripts/get_bot_id.py: -------------------------------------------------------------------------------- 1 | import os 2 | from slackclient import SlackClient 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv(os.path.join(os.path.dirname(__file__), "../.env")) 6 | 7 | BOT_NAME = 'sous-chef' 8 | slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN')) 9 | 10 | if __name__ == "__main__": 11 | api_call = slack_client.api_call("users.list") 12 | if api_call.get('ok'): 13 | # retrieve all users so we can find our bot 14 | users = api_call.get('members') 15 | for user in users: 16 | if 'name' in user and user.get('name') == BOT_NAME: 17 | print("Bot ID for '" + user['name'] + "' is " + user.get('id')) 18 | else: 19 | print("could not find bot user with the name " + BOT_NAME) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | -------------------------------------------------------------------------------- /static/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* style.css 2 | * This file provides css styles. 3 | */ 4 | body, html { 5 | background-color: #3b4b54; 6 | width: 100%; 7 | height: 100%; 8 | margin: 0 auto; 9 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", 10 | "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 11 | color: #ffffff; 12 | } 13 | 14 | a { 15 | text-decoration: none; 16 | color: #00aed1; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | .newappIcon { 24 | padding-top: 10%; 25 | display: block; 26 | margin: 0 auto; 27 | padding-bottom: 2em; 28 | max-width: 200px; 29 | } 30 | 31 | h1 { 32 | font-weight: bold; 33 | font-size: 2em; 34 | } 35 | 36 | .leftHalf { 37 | float: left; 38 | background-color: #26343f; 39 | width: 45%; 40 | height: 100%; 41 | } 42 | 43 | .rightHalf { 44 | float: right; 45 | width: 55%; 46 | background-color: #313f4a; 47 | height: 100%; 48 | overflow: auto; 49 | } 50 | 51 | .description { 52 | padding-left: 50px; 53 | padding-right: 50px; 54 | text-align: center; 55 | font-size: 1.2em; 56 | } 57 | 58 | .blue { 59 | color: #00aed1; 60 | } 61 | 62 | table { 63 | table-layout: fixed; 64 | width: 800px; 65 | margin: 0 auto; 66 | word-wrap: break-word; 67 | padding-top: 10%; 68 | } 69 | 70 | th { 71 | border-bottom: 1px solid #000; 72 | } 73 | 74 | th, td { 75 | text-align: left; 76 | padding: 2px 20px; 77 | } 78 | 79 | .env-var { 80 | text-align: right; 81 | border-right: 1px solid #000; 82 | width: 30%; 83 | } 84 | 85 | pre { 86 | padding: 0; 87 | margin: 0; 88 | } -------------------------------------------------------------------------------- /souschef/recipe.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json, os 3 | from dotenv import load_dotenv 4 | 5 | class RecipeClient: 6 | def __init__(self, api_key): 7 | self. endpoint = \ 8 | 'https://spoonacular-recipe-food-nutrition-v1.p.mashape.com/' 9 | self.api_key = api_key 10 | 11 | def find_by_ingredients(self, ingredients): 12 | url = self.endpoint + 'recipes/findByIngredients' 13 | 14 | params = { 15 | 'fillIngredients': False, 16 | 'ingredients': ingredients, #string 17 | 'limitLicense': False, 18 | 'number': 5, 19 | 'ranking': 1 20 | } 21 | 22 | headers={ 23 | "X-Mashape-Key": self.api_key, 24 | "Accept": "application/json" 25 | } 26 | 27 | return requests.get(url, params=params, headers=headers).json() 28 | 29 | def find_by_cuisine(self, cuisine): 30 | url = self.endpoint + "recipes/search" 31 | 32 | payload = { 33 | 'number': 5, 34 | 'query': ' ', 35 | 'cuisine': cuisine 36 | } 37 | headers={ 'X-Mashape-Key': self.api_key } 38 | 39 | return requests.get(url, 40 | params=payload, 41 | headers=headers).json()['results'] 42 | 43 | def get_info_by_id(self, id): 44 | url = self.endpoint + "recipes/" + str(id) + "/information" 45 | params = {'includeNutrition': False } 46 | headers = {'X-Mashape-Key': self.api_key} 47 | 48 | return requests.get(url, params=params, headers=headers).json() 49 | 50 | def get_steps_by_id(self, id): 51 | url = self.endpoint + "recipes/" + str(id) + "/analyzedInstructions" 52 | params = {'stepBreakdown': True} 53 | headers={'X-Mashape-Key': self.api_key} 54 | 55 | return requests.get(url, params=params, headers=headers).json() 56 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from cloudant.client import Cloudant 5 | from dotenv import load_dotenv 6 | from slackclient import SlackClient 7 | from watson_developer_cloud import ConversationV1 8 | 9 | from souschef.recipe import RecipeClient 10 | from souschef.cloudant_recipe_store import CloudantRecipeStore 11 | from souschef.souschef import SousChef 12 | 13 | if __name__ == "__main__": 14 | try: 15 | # load environment variables 16 | load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) 17 | slack_bot_id = os.environ.get("SLACK_BOT_ID") 18 | slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN')) 19 | conversation_workspace_id = os.environ.get("CONVERSATION_WORKSPACE_ID") 20 | conversation_client = ConversationV1( 21 | username=os.environ.get("CONVERSATION_USERNAME"), 22 | password=os.environ.get("CONVERSATION_PASSWORD"), 23 | version='2016-07-11' 24 | ) 25 | recipe_client = RecipeClient(os.environ.get("SPOONACULAR_KEY")) 26 | recipe_store_url = os.environ.get("CLOUDANT_URL") 27 | if recipe_store_url.find('@') > 0: 28 | prefix = recipe_store_url[0:recipe_store_url.find('://')+3] 29 | suffix = recipe_store_url[recipe_store_url.find('@')+1:] 30 | recipe_store_url = '{}{}'.format(prefix, suffix) 31 | recipe_store = CloudantRecipeStore( 32 | Cloudant( 33 | os.environ.get("CLOUDANT_USERNAME"), 34 | os.environ.get("CLOUDANT_PASSWORD"), 35 | url=recipe_store_url 36 | ), 37 | os.environ.get("CLOUDANT_DB_NAME") 38 | ) 39 | # start the souschef bot 40 | souschef = SousChef(slack_bot_id, 41 | slack_client, 42 | conversation_client, 43 | conversation_workspace_id, 44 | recipe_client, 45 | recipe_store) 46 | souschef.start() 47 | sys.stdin.readline() 48 | except (KeyboardInterrupt, SystemExit): 49 | pass 50 | souschef.stop() 51 | souschef.join() 52 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import deployment_tracker 2 | import os 3 | 4 | from cloudant.client import Cloudant 5 | from dotenv import load_dotenv 6 | from slackclient import SlackClient 7 | from watson_developer_cloud import ConversationV1 8 | 9 | from souschef.recipe import RecipeClient 10 | from souschef.cloudant_recipe_store import CloudantRecipeStore 11 | from souschef.souschef import SousChef 12 | 13 | try: 14 | from SimpleHTTPServer import SimpleHTTPRequestHandler as Handler 15 | from SocketServer import TCPServer as Server 16 | except ImportError: 17 | from http.server import SimpleHTTPRequestHandler as Handler 18 | from http.server import HTTPServer as Server 19 | 20 | # Read port selected by the cloud for our application 21 | PORT = int(os.getenv('PORT', 8000)) 22 | # Change current directory to avoid exposure of control files 23 | os.chdir('static') 24 | 25 | httpd = Server(("", PORT), Handler) 26 | try: 27 | # track deployment 28 | deployment_tracker.track() 29 | # load environment variables 30 | load_dotenv(os.path.join(os.path.dirname(__file__), ".env")) 31 | slack_bot_id = os.environ.get("SLACK_BOT_ID") 32 | slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN')) 33 | conversation_workspace_id = os.environ.get("CONVERSATION_WORKSPACE_ID") 34 | conversation_client = ConversationV1( 35 | username=os.environ.get("CONVERSATION_USERNAME"), 36 | password=os.environ.get("CONVERSATION_PASSWORD"), 37 | version='2016-07-11' 38 | ) 39 | recipe_client = RecipeClient(os.environ.get("SPOONACULAR_KEY")) 40 | recipe_store_url = os.environ.get("CLOUDANT_URL") 41 | if recipe_store_url.find('@') > 0: 42 | prefix = recipe_store_url[0:recipe_store_url.find('://')+3] 43 | suffix = recipe_store_url[recipe_store_url.find('@')+1:] 44 | recipe_store_url = '{}{}'.format(prefix, suffix) 45 | recipe_store = CloudantRecipeStore( 46 | Cloudant( 47 | os.environ.get("CLOUDANT_USERNAME"), 48 | os.environ.get("CLOUDANT_PASSWORD"), 49 | url=recipe_store_url 50 | ), 51 | os.environ.get("CLOUDANT_DB_NAME") 52 | ) 53 | # start the souschef bot 54 | souschef = SousChef(slack_bot_id, 55 | slack_client, 56 | conversation_client, 57 | conversation_workspace_id, 58 | recipe_client, 59 | recipe_store) 60 | souschef.start() 61 | # start the http server 62 | print("Start serving at port %i" % PORT) 63 | httpd.serve_forever() 64 | except (KeyboardInterrupt, SystemExit): 65 | pass 66 | souschef.stop() 67 | souschef.join() 68 | httpd.server_close() 69 | -------------------------------------------------------------------------------- /deployment_tracker.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from re import search 4 | from requests import post 5 | from os import environ as env 6 | 7 | 8 | def track(tracker_url=None): 9 | 10 | # version and repository URL 11 | version = '0.0.1' 12 | repo_url = 'https://github.com/ibm-cds-labs/watson-recipe-bot-python-cloudant' 13 | 14 | # get vcap application details 15 | if env.get('VCAP_APPLICATION') is not None: 16 | vcap_app = json.loads(env['VCAP_APPLICATION']) 17 | event = dict() 18 | event['date_sent'] = time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()) 19 | if version is not None: 20 | event['code_version'] = version 21 | if repo_url is not None: 22 | event['repository_url'] = repo_url 23 | event['runtime'] = 'python' 24 | event['application_name'] = str(vcap_app['name']) 25 | event['space_id'] = str(vcap_app['space_id']) 26 | event['application_version'] = str(vcap_app['application_version']) 27 | event['application_uris'] = [str(uri) for uri in vcap_app['application_uris']] 28 | 29 | # Check for VCAP_SERVICES env var with at least one service 30 | # Refer to http://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html#VCAP-SERVICES 31 | if env.get('VCAP_SERVICES') is not None and json.loads(env['VCAP_SERVICES']): 32 | vcap_services = json.loads(env['VCAP_SERVICES']) 33 | event['bound_vcap_services'] = dict() 34 | 35 | # For each bound service, count the number of instances and identify used plans 36 | for service in vcap_services: 37 | event['bound_vcap_services'][service] = { 38 | 'count': len(vcap_services[service]), 39 | 'plans': [] 40 | } 41 | 42 | # Append plans for each instance 43 | for instance in vcap_services[service]: 44 | if 'plan' in instance.keys(): 45 | event['bound_vcap_services'][service]['plans'].append(str(instance['plan'])) 46 | 47 | if len(event['bound_vcap_services'][service]['plans']) == 0: 48 | del event['bound_vcap_services'][service]['plans'] 49 | 50 | # Create and format request to Deployment Tracker 51 | url = 'https://deployment-tracker.mybluemix.net/api/v1/track' if tracker_url is None else tracker_url 52 | headers = {'content-type': "application/json"} 53 | try: 54 | print ('Uploading stats to Deployment Tracker: {}'.format(json.dumps(event))) 55 | response = post(url, data=json.dumps(event), headers=headers) 56 | print ('Uploaded stats: %s' % response.text) 57 | except Exception as e: 58 | print ('Deployment Tracker upload error: %s' % str(e)) -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | {"name":"Recipe Bot","created":"2016-11-10T20:23:34.362Z","intents":[{"intent":"favorite_recipes","created":"2016-11-18T15:19:18.424Z","updated":"2016-12-01T17:45:47.138Z","examples":[{"text":"I want to cook one of my favorites","created":"2016-11-18T15:19:18.424Z","updated":"2016-11-29T17:28:34.396Z"},{"text":"I want to cook something I've cooked before","created":"2016-11-18T15:19:18.424Z","updated":"2016-11-29T17:28:34.396Z"},{"text":"Show me my favorite recipes","created":"2016-11-18T15:19:18.424Z","updated":"2016-11-29T17:28:34.396Z"},{"text":"What are my favorite recipes","created":"2016-11-18T15:19:18.424Z","updated":"2016-11-29T17:28:34.396Z"},{"text":"What have I cooked before","created":"2016-11-18T15:19:18.424Z","updated":"2016-11-29T17:28:34.396Z"},{"text":"What have I cooked in the past","created":"2016-11-18T15:19:18.424Z","updated":"2016-11-29T17:28:34.396Z"}],"description":null},{"intent":"no","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","examples":[{"text":"nah","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:25:17.123Z"},{"text":"no","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:25:17.123Z"},{"text":"nope","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:25:17.123Z"},{"text":"no thanks","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:25:17.123Z"},{"text":"no way","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:25:17.123Z"}],"description":null},{"intent":"start_cooking","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","examples":[{"text":"give me a recipe","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"I don't know what to cook","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"i need a recipe suggestion","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"i want a recipe","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"i want to cook","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"i want to cook something","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"let's cook","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"lets cook something","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"what's a good recipe","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"what should I cook?","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"},{"text":"what should I eat?","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:45:45.271Z"}],"description":null},{"intent":"yes","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:24:11.925Z","examples":[{"text":"ok","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:31:45.525Z"},{"text":"sure","created":"2016-11-10T20:24:11.925Z","updated":"2016-11-29T17:31:45.525Z"},{"text":"yas","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:31:45.525Z"},{"text":"yeah","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:31:45.525Z"},{"text":"yes","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:31:45.525Z"},{"text":"yup","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-29T17:31:45.525Z"}],"description":null}],"updated":"2016-12-01T17:45:47.138Z","entities":[{"type":null,"entity":"cuisine","source":null,"values":[{"value":"african","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"american","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"british","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"cajun","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"caribbean","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"chinese","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"eastern european","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"french","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"german","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"greek","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"indian","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"irish","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"italian","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"japanese","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"jewish","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"korean","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"latin american","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"mexican","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"middle eastern","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"nordic","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"southern","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"spanish","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"thai","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]},{"value":"vietnamese","created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"synonyms":[]}],"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","open_list":false,"description":null}],"language":"en","metadata":{"runtime_version":"2016-09-20"},"description":"Conversational bot for getting recipes.","dialog_nodes":[{"go_to":null,"output":{"text":"Enter your selection:"},"parent":"node_5_1470995072447","context":{"selection":"","is_selection":true,"is_ingredients":false},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-18T15:54:16.448Z","metadata":null,"conditions":"true","description":null,"dialog_node":"node_21_1471330497996","previous_sibling":null},{"go_to":null,"output":{"text":"Great, give me the list of ingredients (comma separated).\n\nE.g. onions, tomatoes, cilantro, beef"},"parent":"node_2_1470200792636","context":{"get_recipes":true},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"conditions":"#yes","description":null,"dialog_node":"node_4_1470201214332","previous_sibling":"node_2_1470201812278"},{"go_to":null,"output":{"text":"Here is what I've found..."},"parent":"node_4_1470201214332","context":{"is_ingredients":true},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"conditions":"true","description":null,"dialog_node":"node_5_1470995072447","previous_sibling":null},{"go_to":null,"output":{"text":"Hi, I'm the Watson RecipeBot. I know a lot about recipes. How can I help you?"},"parent":null,"context":null,"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-18T15:33:50.927Z","metadata":null,"conditions":"conversation_start","description":null,"dialog_node":"node_1_1470199483860","previous_sibling":null},{"go_to":null,"output":{"text":"I can help with that! Are you looking to use specific ingredients?"},"parent":"node_1_1470199483860","context":null,"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-30T15:11:40.317Z","metadata":null,"conditions":"#start_cooking","description":null,"dialog_node":"node_2_1470200792636","previous_sibling":"node_3_1479482731826"},{"go_to":null,"output":{"text":"Let me see what I can find."},"parent":"node_1_1470199483860","context":{"is_favorites":true},"created":"2016-11-18T15:25:32.046Z","updated":"2016-11-18T15:53:58.584Z","metadata":null,"conditions":"#favorite_recipes","description":null,"dialog_node":"node_3_1479482731826","previous_sibling":null},{"go_to":null,"output":{"text":"Ok, why don't you give me a type of cuisine you'd like to cook.\n\nE.g. chinese, italian"},"parent":"node_2_1470200792636","context":{"get_recipes":true},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:26:04.385Z","metadata":null,"conditions":"#no","description":null,"dialog_node":"node_2_1470201812278","previous_sibling":null},{"go_to":null,"output":{"text":"Thanks, let's see what I can find."},"parent":"node_2_1470201812278","context":null,"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-18T15:33:15.717Z","metadata":null,"conditions":"@cuisine","description":null,"dialog_node":"node_2_1470991098022","previous_sibling":null},{"go_to":{"return":null,"selector":"body","dialog_node":"node_1_1470199483860"},"output":{"text":"I'm sorry, I'm only interested in recipes. I hear Siri likes to banter."},"parent":null,"context":null,"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-18T15:25:19.372Z","metadata":null,"conditions":"anything_else","description":null,"dialog_node":"node_2_1470200489267","previous_sibling":"node_1_1470199483860"},{"go_to":{"return":null,"selector":"body","dialog_node":"node_1_1470199483860"},"output":{"text":"True!"},"parent":"node_21_1471330497996","context":{"is_selection":false},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"conditions":"$selection_valid == true","description":null,"dialog_node":"node_22_1471330823481","previous_sibling":null},{"go_to":{"return":null,"selector":"body","dialog_node":"node_2_1470201812278"},"output":{"text":"Sorry, I don't recognize the cuisine. Try another type."},"parent":"node_2_1470201812278","context":null,"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-18T15:33:13.091Z","metadata":null,"conditions":"true","description":null,"dialog_node":"node_4_1470990774679","previous_sibling":"node_2_1470991098022"},{"go_to":{"return":null,"selector":"body","dialog_node":"node_5_1470995072447"},"output":{"text":"False!"},"parent":"node_21_1471330497996","context":{"get_recipes":false},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"conditions":"$selection_valid == false","description":null,"dialog_node":"node_23_1471337556367","previous_sibling":"node_22_1471330823481"},{"go_to":{"return":null,"selector":"condition","dialog_node":"node_21_1471330497996"},"output":{"text":"Here are your favorite recipes..."},"parent":"node_3_1479482731826","context":{"selection":"","is_favorites":false,"is_selection":true,"is_ingredients":false},"created":"2016-11-18T15:52:47.217Z","updated":"2016-11-18T15:54:16.788Z","metadata":null,"conditions":"true","description":null,"dialog_node":"node_4_1479484366912","previous_sibling":null},{"go_to":{"return":null,"selector":"condition","dialog_node":"node_21_1471330497996"},"output":{"text":"Here are your results based on cuisine."},"parent":"node_2_1470991098022","context":{"selection":"","is_selection":true,"is_ingredients":false},"created":"2016-11-10T20:23:34.362Z","updated":"2016-11-10T20:23:34.362Z","metadata":null,"conditions":"true","description":null,"dialog_node":"node_25_1471342933041","previous_sibling":null}],"workspace_id":"a0b5c7d5-5294-464a-8b68-198d618c8e44","counterexamples":[]} -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 IBM Corporation 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /souschef/souschef.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import sys 3 | import time 4 | import threading 5 | from .user_state import UserState 6 | 7 | 8 | class SousChef(threading.Thread): 9 | 10 | def __init__(self, slack_bot_id, slack_client, conversation_client, conversation_workspace_id, recipe_client, recipe_store): 11 | threading.Thread.__init__(self) 12 | self.running = True 13 | self.slack_bot_id = slack_bot_id 14 | self.slack_client = slack_client 15 | self.conversation_client = conversation_client 16 | self.conversation_workspace_id = conversation_workspace_id 17 | self.recipe_client = recipe_client 18 | self.recipe_store = recipe_store 19 | # 20 | self.at_bot = "<@" + slack_bot_id + ">:" 21 | self.delay = 0.5 # second 22 | self.user_state_map = {} 23 | self.pp = pprint.PrettyPrinter(indent=4) 24 | 25 | def parse_slack_output(self, slack_rtm_output): 26 | output_list = slack_rtm_output 27 | if output_list and len(output_list) > 0: 28 | for output in output_list: 29 | if output and 'text' in output and 'user_profile' not in output and self.at_bot in output['text']: 30 | return output['text'].split(self.at_bot)[1].strip().lower(), output['user'], output['channel'] 31 | elif output and 'text' in output and 'user_profile' not in output: 32 | return output['text'].lower(), output['user'], output['channel'] 33 | return None, None, None 34 | 35 | def post_to_slack(self, response, channel): 36 | self.slack_client.api_call("chat.postMessage", channel=channel, text=response, as_user=True) 37 | 38 | def handle_message(self, message, message_sender, channel): 39 | try: 40 | # get or create state for the user 41 | if message_sender in self.user_state_map.keys(): 42 | state = self.user_state_map[message_sender] 43 | else: 44 | state = UserState(message_sender) 45 | self.user_state_map[message_sender] = state 46 | # send message to watson conversation 47 | watson_response = self.conversation_client.message( 48 | workspace_id=self.conversation_workspace_id, 49 | message_input={'text': message}, 50 | context=state.conversation_context) 51 | # update conversation context 52 | state.conversation_context = watson_response['context'] 53 | # route response 54 | if 'is_favorites' in state.conversation_context.keys() and state.conversation_context['is_favorites']: 55 | response = self.handle_favorites_message(state) 56 | elif 'is_ingredients' in state.conversation_context.keys() and state.conversation_context['is_ingredients']: 57 | response = self.handle_ingredients_message(state, message) 58 | elif 'is_selection' in state.conversation_context.keys() and state.conversation_context['is_selection']: 59 | response = self.handle_selection_message(state) 60 | elif watson_response['entities'] and watson_response['entities'][0]['entity'] == 'cuisine': 61 | cuisine = watson_response['entities'][0]['value'] 62 | response = self.handle_cuisine_message(state, cuisine) 63 | else: 64 | response = self.handle_start_message(state, watson_response) 65 | except Exception: 66 | print(sys.exc_info()) 67 | # clear state and set response 68 | self.clear_user_state(state) 69 | response = "Sorry, something went wrong! Say anything to me to start over..." 70 | # post response to slack 71 | self.post_to_slack(response, channel) 72 | 73 | def handle_start_message(self, state, watson_response): 74 | if state.user is None: 75 | user = self.recipe_store.add_user(state.user_id) 76 | state.user = user 77 | response = '' 78 | for text in watson_response['output']['text']: 79 | response += text + "\n" 80 | return response 81 | 82 | def handle_favorites_message(self, state): 83 | recipes = self.recipe_store.find_favorite_recipes_for_user(state.user, 5) 84 | # update state 85 | state.conversation_context['recipes'] = recipes 86 | state.ingredient_cuisine = None 87 | # build and return response 88 | response = self.get_recipe_list_response(state) 89 | return response 90 | 91 | def handle_ingredients_message(self, state, message): 92 | # we want to get a list of recipes based on the ingredients (message) 93 | # first we see if we already have the ingredients in our datastore 94 | ingredients_str = message 95 | ingredient = self.recipe_store.find_ingredient(ingredients_str) 96 | if ingredient is not None: 97 | print("Ingredient exists for {}. Returning recipes from datastore.".format(ingredients_str)) 98 | matching_recipes = ingredient['recipes'] 99 | # increment the count on the user-ingredient 100 | self.recipe_store.record_ingredient_request_for_user(ingredient, state.user) 101 | else: 102 | # we don't have the ingredients in our datastore yet, so get list of recipes from Spoonacular 103 | print("Ingredient does not exist for {}. Querying Spoonacular for recipes.".format(ingredients_str)) 104 | matching_recipes = self.recipe_client.find_by_ingredients(ingredients_str) 105 | # add ingredient to datastore 106 | ingredient = self.recipe_store.add_ingredient(ingredients_str, matching_recipes, state.user) 107 | # update state 108 | state.conversation_context['recipes'] = matching_recipes 109 | state.ingredient_cuisine = ingredient 110 | # build and return response 111 | response = self.get_recipe_list_response(state) 112 | return response 113 | 114 | def handle_cuisine_message(self, state, message): 115 | # we want to get a list of recipes based on the cuisine 116 | # first we see if we already have the cuisine in our datastore 117 | cuisine_str = message 118 | cuisine = self.recipe_store.find_cuisine(cuisine_str) 119 | if cuisine is not None: 120 | print("Cuisine exists for {}. Returning recipes from datastore.".format(cuisine_str)) 121 | matching_recipes = cuisine['recipes'] 122 | # increment the count on the user-cuisine 123 | self.recipe_store.record_cuisine_request_for_user(cuisine, state.user) 124 | else: 125 | # we don't have the cuisine in our datastore yet, so get list of recipes from Spoonacular 126 | print("Cuisine does not exist for {}. Querying Spoonacular for recipes.".format(cuisine_str)) 127 | matching_recipes = self.recipe_client.find_by_cuisine(cuisine_str) 128 | # add cuisine to datastore 129 | cuisine = self.recipe_store.add_cuisine(cuisine_str, matching_recipes, state.user) 130 | # update state 131 | state.conversation_context['recipes'] = matching_recipes 132 | state.ingredient_cuisine = cuisine 133 | # build and return response 134 | response = self.get_recipe_list_response(state) 135 | return response 136 | 137 | def handle_selection_message(self, state): 138 | selection = -1 139 | if state.conversation_context['selection'].isdigit(): 140 | selection = int(state.conversation_context['selection']) 141 | if 1 <= selection <= 5: 142 | # we want to get a the recipe based on the selection 143 | # first we see if we already have the recipe in our datastore 144 | recipes = state.conversation_context['recipes'] 145 | recipe_id = recipes[selection-1]['id'] 146 | recipe = self.recipe_store.find_recipe(recipe_id) 147 | if recipe is not None: 148 | print("Recipe exists for {}. Returning recipe steps from datastore.".format(recipe_id)) 149 | recipe_detail = recipe['instructions'] 150 | recipe_title = recipe['title'] 151 | # increment the count on the ingredient/cuisine-recipe and the user-recipe 152 | self.recipe_store.record_recipe_request_for_user(recipe, state.ingredient_cuisine, state.user) 153 | else: 154 | print("Recipe does not exist for {}. Querying Spoonacular for details.".format(recipe_id)) 155 | recipe_info = self.recipe_client.get_info_by_id(recipe_id) 156 | recipe_steps = self.recipe_client.get_steps_by_id(recipe_id) 157 | recipe_detail = self.get_recipe_instructions_response(recipe_info, recipe_steps) 158 | recipe_title = recipe_info['title'] 159 | # add recipe to datastore 160 | self.recipe_store.add_recipe(recipe_id, recipe_title, recipe_detail, state.ingredient_cuisine, state.user) 161 | # clear state and return response 162 | self.clear_user_state(state) 163 | return recipe_detail 164 | else: 165 | # clear state and return response 166 | self.clear_user_state(state) 167 | return "Invalid selection! Say anything to start over..." 168 | 169 | @staticmethod 170 | def clear_user_state(state): 171 | state.ingredient_cuisine = None 172 | state.conversation_context = None 173 | state.conversation_started = False 174 | 175 | @staticmethod 176 | def get_recipe_list_response(state): 177 | response = "Lets see here...\nI've found these recipes:\n" 178 | for i, recipe in enumerate(state.conversation_context['recipes']): 179 | response += str(i + 1) + ". " + recipe['title'] + "\n" 180 | response += "\nPlease enter the corresponding number of your choice." 181 | return response 182 | 183 | @staticmethod 184 | def get_recipe_instructions_response(recipe_info, recipe_steps): 185 | response = "Ok, it takes *" + \ 186 | str(recipe_info['readyInMinutes']) + \ 187 | "* minutes to make *" + \ 188 | str(recipe_info['servings']) + \ 189 | "* servings of *" + \ 190 | recipe_info['title'] + "*. Here are the steps:\n\n" 191 | 192 | if recipe_steps and recipe_steps[0]['steps']: 193 | for i, r_step in enumerate(recipe_steps[0]['steps']): 194 | equip_str = "" 195 | for e in r_step['equipment']: 196 | equip_str += e['name'] + ", " 197 | if not equip_str: 198 | equip_str = "None" 199 | else: 200 | equip_str = equip_str[:-2] 201 | response += "*Step " + str(i + 1) + "*:\n" + \ 202 | "_Equipment_: " + equip_str + "\n" + \ 203 | "_Action_: " + r_step['step'] + "\n\n" 204 | else: 205 | response += "_No instructions available for this recipe._\n\n" 206 | 207 | response += "*Say anything to me to start over...*" 208 | return response 209 | 210 | def run(self): 211 | self.recipe_store.init() 212 | while self.running: 213 | if self.slack_client.rtm_connect(): 214 | print("sous-chef is connected and running!") 215 | while self.running: 216 | slack_output = self.slack_client.rtm_read() 217 | message, message_sender, channel = self.parse_slack_output(slack_output) 218 | if message and channel and message_sender != self.slack_bot_id: 219 | self.handle_message(message, message_sender, channel) 220 | time.sleep(self.delay) 221 | else: 222 | print("Connection failed. Invalid Slack token or bot ID?") 223 | print("sous-chef shutting down...") 224 | 225 | def stop(self): 226 | self.running = False 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :no_entry_sign: This project is no longer maintained 2 | 3 | # Watson Recipe Bot + Cloudant 4 | 5 | This project is based on the [Watson Recipe Bot example](https://medium.com/ibm-watson-developer-cloud/how-to-build-a-recipe-slack-bot-using-watson-conversation-and-spoonacular-api-487eacaf01d4#.i0q8fnhuu). 6 | The Watson Recipe Bot is a Slack bot that recommends recipes based on ingredients or cuisines. 7 | This project is essentially a fork of the Watson Recipe Bot with some additional features, including: 8 | 9 | 1. Multi-user support - the original application supported only a single user interacting with the bot at a time. This application supports multiple users interacting with the bot at the same time. 10 | 2. Deploy to Bluemix - the original application was designed to be run locally. This application can be run locally, or deployed as a web application to Bluemix. 11 | 2. Cloudant integration - this application adds Cloudant integration for caching 3rd party API calls and storing each user's chat history (the ingredients, cuisines, and recipes they have selected). 12 | 3. Additional Watson Conversation intent - this application adds a "favorites" intent which allows a user to request their favorite recipes based on the history stored in Cloudant. 13 | 14 | ####Prefer Node.js? 15 | 16 | There is a Node.js version of this project [here](https://github.com/ibm-cds-labs/watson-recipe-bot-nodejs-cloudant). 17 | 18 | ## Getting Started 19 | 20 | Before you get started [read the original blog post](https://medium.com/ibm-watson-developer-cloud/how-to-build-a-recipe-slack-bot-using-watson-conversation-and-spoonacular-api-487eacaf01d4#.i0q8fnhuu) 21 | to understand how the Watson Recipe Bot works. You __do not__ need to follow the instructions in the blog post. All the instructions required to run the bot are below. 22 | After cloning this repo follow the steps below. 23 | 24 | ### Quick Reference 25 | 26 | The following environment variables are required to run the application: 27 | 28 | ``` 29 | SLACK_BOT_TOKEN=xxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx 30 | SLACK_BOT_ID=UXXXXXXXX 31 | SPOONACULAR_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 32 | CONVERSATION_USERNAME=xxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxxx 33 | CONVERSATION_PASSWORD=xxxxxxxxxxxx 34 | CONVERSATION_WORKSPACE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 35 | CLOUDANT_USERNAME=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-bluemix 36 | CLOUDANT_PASSWORD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 37 | CLOUDANT_URL=https://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-bluemix.cloudant.com 38 | CLOUDANT_DB_NAME=watson_recipe_bot 39 | ``` 40 | 41 | We will show you how to configure the necessary services and retrieve these values in the instructions below: 42 | 43 | ### Prerequisites 44 | 45 | The following prerequisites are required to run the application. 46 | 47 | 1. A [Bluemix](https://www.ibm.com/cloud-computing/bluemix/) account. 48 | 2. A [Watson Conversation](https://www.ibm.com/watson/developercloud/conversation.html) service provisioned in your Bluemix account. 49 | 3. A [Cloudant](http://cloudant.com/) service provisioned in your Bluemix account. 50 | 4. A [Spoonacular](https://spoonacular.com/food-api) API key. A free tier is available, however a credit card is required. 51 | 5. A [Slack](https://slack.com) account and permission in your Slack team to register a Slack bot. 52 | 53 | To run locally you will need Python 2 or 3, [pip](https://pip.pypa.io/en/stable/) and [virtualenv](https://virtualenv.pypa.io/en/stable/). 54 | 55 | To push your application to Bluemix from your local development environment you will need the [Bluemix CLI and Dev Tools](https://console.ng.bluemix.net/docs/starters/install_cli.html). 56 | 57 | ### Local Development Environment 58 | 59 | We'll start by getting your local development environment set up. If you haven't already install Python, pip, and virtualenv. 60 | 61 | You can install Python by following the instructions [here](https://www.python.org/downloads/). 62 | 63 | You can install pip by following the instructions [here](https://pip.pypa.io/en/stable/). 64 | 65 | You can install virtualenv by following the instructions [here](https://virtualenv.pypa.io/en/stable/). 66 | 67 | From the command-line cd into the watson-recipe-bot-python-cloudant directory: 68 | 69 | ``` 70 | git clone https://github.com/ibm-cds-labs/watson-recipe-bot-python-cloudant 71 | cd watson-recipe-bot-python-cloudant 72 | ``` 73 | 74 | Create and activate a new virtual environment: 75 | 76 | ``` 77 | virtualenv venv 78 | source ./venv/bin/activate 79 | ``` 80 | 81 | Install the application requirements: 82 | 83 | ``` 84 | pip install -r requirements.txt 85 | ``` 86 | 87 | Copy the .env.template file included in the project to .env. This file will contain the environment variable definitions: 88 | 89 | ``` 90 | cp .env.template .env 91 | ``` 92 | 93 | ### Slack 94 | 95 | In this next step we'll create a new Slack bot in your Slack team. 96 | 97 | In your web browser go to [https://my.slack.com/services/new/bot](https://my.slack.com/services/new/bot). Make sure you sign into the appropriate Slack team. 98 | You can also change the Slack team from the pulldown in the top right. 99 | 100 | 1. You'll start by choosing a username for your bot. In the field provided enter **sous-chef**. 101 | 102 | ![Slack](screenshots/slack1.png?rev=2&raw=true) 103 | 104 | 2. Click the **Add bot integration** button. 105 | 3. On the following screen you will find the API Token. Copy this value to your clipboard. 106 | 107 | ![Slack](screenshots/slack2.png?rev=2&raw=true) 108 | 109 | 4. Open the .env file in a text editor. 110 | 5. Paste the copied token from your clipboard as the SLACK_BOT_TOKEN value: 111 | 112 | ``` 113 | SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx 114 | ``` 115 | 116 | 6. Save the .env file 117 | 118 | Next, we need to get the Slack ID of the bot. 119 | 120 | 1. From the command-line run the following command: 121 | 122 | ``` 123 | python scripts/get_bot_id.py 124 | ``` 125 | 126 | 2. The script should print out the bot ID. The output should be similar to the following: 127 | 128 | ``` 129 | Bot ID for 'sous-chef' is U3XXXXXXX 130 | ``` 131 | 132 | 3. Copy and paste the bot ID into your .env file: 133 | 134 | ``` 135 | SLACK_BOT_ID=U3XXXXXXX 136 | ``` 137 | 138 | ### Spoonacular 139 | 140 | In this next step we'll set up your Spoonacular account. Spoonacular is a Food and Recipe API. 141 | The application uses Spoonacular to find recipes based on ingredient or cuisines requested by the user. 142 | 143 | 1. In your web browser go to [https://spoonacular.com/food-api](https://spoonacular.com/food-api). 144 | 2. Click the **Get Access** button. 145 | 146 | ![Spoonacular](screenshots/spoonacular1.png?rev=1&raw=true) 147 | 148 | 3. Click the appropriate button to gain access (i.e. **Get Regular Access**) 149 | 150 | ![Spoonacular](screenshots/spoonacular2.png?rev=2&raw=true) 151 | 152 | 4. Choose the appropriate Pricing plan (i.e. **Basic**) and click the **Subscribe** button. 153 | 5. Follow the instructions to sign into or sign up for a Mashape account. 154 | 6. After you have subscribed to Spoonacular in the Documentation tab find a curl example on the right. It should look similar to this: 155 | 156 | ![Spoonacular](screenshots/spoonacular3.png?rev=2&raw=true) 157 | 158 | 7. Copy the value of the X-Mashape-Key and paste it into your .env file: 159 | 160 | ``` 161 | SPOONACULAR_KEY=vxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 162 | ``` 163 | 164 | ### Bluemix 165 | 166 | If you do not already have a Bluemix account [click here](https://console.ng.bluemix.net/registration/) to sign up. 167 | 168 | Login to your Bluemix account. 169 | 170 | ### Watson Conversation 171 | 172 | First, we'll walk you through provisioning a Watson Conversation service in your Bluemix account: 173 | 174 | 1. From your Bluemix Applications or Services Dashboard click the **Create Service** button. 175 | 176 | ![Bluemix](screenshots/bluemix1.png?rev=3&raw=true) 177 | 178 | 2. In the IBM Bluemix Catalog search for **Watson Conversation**. 179 | 3. Select the **Conversation** service. 180 | 181 | ![Watson Conversation](screenshots/conversation1.png?rev=1&raw=true) 182 | 183 | 4. Click the **Create** button on the Conversation detail page. 184 | 5. On your newly created Conversation service page click the **Service Credentials** tab. 185 | 186 | ![Watson Conversation](screenshots/conversation2.png?rev=1&raw=true) 187 | 188 | 6. Find your newly created Credentials and click **View Credentials** 189 | 190 | ![Watson Conversation](screenshots/conversation3.png?rev=1&raw=true) 191 | 192 | 7. Copy the username and password into your .env file: 193 | 194 | ``` 195 | CONVERSATION_USERNAME=xxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxxxxxxx 196 | CONVERSATION_PASSWORD=xxxxxxxxxxxx 197 | ``` 198 | 199 | Next, let's launch the Watson Conversation tool and import our conversation workspace. 200 | 201 | 1. Go back to the **Manage** tab. 202 | 2. Click the **Launch tool** button. 203 | 204 | ![Watson Conversation](screenshots/conversation4.png?rev=1&raw=true) 205 | 206 | 3. Log in to Watson Conversation with your Bluemix credentials if prompted to do so. 207 | 4. On the **Create workspace** page click the **Import** button. 208 | 209 | ![Watson Conversation](screenshots/conversation5.png?rev=1&raw=true) 210 | 211 | 5. Choose the workspace.json file in the application directory (*watson-recipe-bot-python-cloudant/workspace.json*). 212 | 6. Click the **Import** button. 213 | 214 | ![Watson Conversation](screenshots/conversation6.png?rev=1&raw=true) 215 | 216 | 7. Under Workspaces you should now see the Recipe Bot. 217 | 8. Click the menu button (3 vertical dots) and click **View Details** 218 | 219 | ![Watson Conversation](screenshots/conversation7.png?rev=1&raw=true) 220 | 221 | 9. Copy the Workspace ID and paste it into your .env file: 222 | 223 | ``` 224 | CONVERSATION_WORKSPACE_ID=40xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 225 | ``` 226 | 227 | ### Cloudant 228 | 229 | We're almost there! Next, we'll provision an instance of Cloudant in our Bluemix account. After this step we will be able to run our bot locally. 230 | 231 | 1. From your Bluemix Applications or Services Dashboard click the **Create Service** button. 232 | 2. In the IBM Bluemix Catalog search for **Cloudant**. 233 | 3. Select the **Cloudant NoSQL DB** service. 234 | 235 | ![Watson Conversation](screenshots/cloudant1.png?rev=1&raw=true) 236 | 237 | 4. Click the **Create** button on the Cloudant detail page. 238 | 5. On your newly created Cloudant service page click the **Service Credentials** tab. 239 | 6. Find your newly created Credentials and click **View Credentials** 240 | 7. Copy the username, password, and the url into your .env file: 241 | 242 | ``` 243 | CLOUDANT_USERNAME=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-bluemix 244 | CLOUDANT_PASSWORD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 245 | CLOUDANT_URL=https://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-bluemix.cloudant.com 246 | ``` 247 | 248 | ### Run Locally 249 | 250 | We're now ready to test our bot. From the command-line run the following command: 251 | 252 | ``` 253 | python run.py 254 | ``` 255 | 256 | If all is well you should see output similar to the following: 257 | 258 | ``` 259 | Getting database... 260 | Creating database watson_recipe_bot... 261 | sous-chef is connected and running! 262 | ``` 263 | 264 | To interact with the bot open Slack, go to the Slack team where you installed the bot, start a direct conversation with 265 | sous-chef, and say "hi". 266 | 267 | ![sous-chef](screenshots/local1.png?rev=2&raw=true) 268 | 269 | ### Deploy to Bluemix 270 | 271 | You can deploy the application to Bluemix by following a few simple steps. 272 | 273 | We will be using the `cf push` command to deploy the application to Bluemix from our local environment. 274 | If you have not yet installed the Bluemix CLI and Dev Tools [click here](https://console.ng.bluemix.net/docs/starters/install_cli.html) 275 | for instructions on how to download and configure the CLI tools. 276 | 277 | Once you have the CLI tools run the following command (make sure you are in the watson-recipe-bot-python-cloudant directory): 278 | 279 | ``` 280 | cf push 281 | ``` 282 | 283 | It will take a few minutes for the application to be uploaded and created in Bluemix. 284 | The first time you deploy the application it will fail to start. 285 | This is because we need to add our environment variables to the application in Bluemix. 286 | 287 | 1. After a few minutes has passed and your `cf push` command has completed log in to Bluemix. 288 | 2. Find and click the **watson-recipe-bot-cloudant** application under **Cloud Foundry Applications** in your Apps Dashboard. 289 | 3. On the application page click **Runtime** in the menu on the left then click **Environment Variables**: 290 | 291 | ![Cloud Foundry Application](screenshots/cfapp1.png?rev=1&raw=true) 292 | 293 | 4. Add each environment variable from your .env file and click the **Save** button: 294 | 295 | ![Cloud Foundry Application](screenshots/cfapp2.png?rev=1&raw=true) 296 | 297 | 5. Your app will automatically restart, but it may fail again. Wait for a minute or two and restart your app again. 298 | 299 | To verify that your bot is running open Slack and start a direct conversation with sous-chef. Slack should show that sous-chef is active: 300 | 301 | ![sous-chef](screenshots/sous-chef1.png?rev=1&raw=true) 302 | 303 | Here are some sample conversations you can have with sous-chef: 304 | 305 | ![sous-chef](screenshots/sous-chef-convo1.png?rev=4&raw=true) 306 | 307 | ![sous-chef](screenshots/sous-chef-convo2.png?rev=4&raw=true) 308 | 309 | ![sous-chef](screenshots/sous-chef-convo3.png?rev=4&raw=true) 310 | 311 | ## Next Steps 312 | 313 | For more information on how the sous-chef bot works [read the original blog post](https://medium.com/ibm-watson-developer-cloud/how-to-build-a-recipe-slack-bot-using-watson-conversation-and-spoonacular-api-487eacaf01d4#.i0q8fnhuu). 314 | 315 | We will be publishing a new blog post soon that will discuss the enhancements we have made to the original application, including 316 | how we are using Cloudant to store chat history and enable the new "favorites" intent. 317 | 318 | ## Privacy Notice 319 | 320 | This application includes code to track deployments to [IBM Bluemix](https://www.bluemix.net/) and other Cloud Foundry platforms. The following information is sent to a [Deployment Tracker](https://github.com/cloudant-labs/deployment-tracker) service on each deployment: 321 | 322 | * Application Name (`application_name`) 323 | * Space ID (`space_id`) 324 | * Application Version (`application_version`) 325 | * Application URIs (`application_uris`) 326 | 327 | This data is collected from the `VCAP_APPLICATION` environment variable in IBM Bluemix and other Cloud Foundry platforms. This data is used by IBM to track metrics around deployments of sample applications to IBM Bluemix to measure the usefulness of our examples, so that we can continuously improve the content we offer to you. Only deployments of sample applications that include code to ping the Deployment Tracker service will be tracked. 328 | 329 | ### Disabling Deployment Tracking 330 | 331 | Deployment tracking can be disabled by removing or commenting out the following line in `server.py`: 332 | 333 | `deployment_tracker.track()` 334 | 335 | ## License 336 | 337 | Licensed under the [Apache License, Version 2.0](LICENSE.txt). 338 | -------------------------------------------------------------------------------- /souschef/cloudant_recipe_store.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from cloudant.query import Query 4 | 5 | 6 | class CloudantRecipeStore(object): 7 | 8 | def __init__(self, client, db_name): 9 | """ 10 | Creates a new instance of CloudantRecipeStore. 11 | Parameters 12 | ---------- 13 | client - The instance of cloudant client to connect to 14 | db_name - The name of the database to use 15 | """ 16 | self.client = client 17 | self.db_name = db_name 18 | 19 | def init(self): 20 | """ 21 | Creates and initializes the database. 22 | """ 23 | try: 24 | self.client.connect() 25 | print('Getting database...') 26 | if self.db_name not in self.client.all_dbs(): 27 | print('Creating database {}...'.format(self.db_name)) 28 | self.client.create_database(self.db_name) 29 | else: 30 | print('Database {} exists.'.format(self.db_name)) 31 | # see if the by_popularity design doc exists, if not then create it 32 | db = self.client[self.db_name] 33 | query = Query(db, selector={ '_id': '_design/by_popularity' }) 34 | result = query()['docs'] 35 | if result is None or len(result) <= 0: 36 | design_doc = { 37 | '_id': '_design/by_popularity', 38 | 'views': { 39 | 'ingredients': { 40 | 'map': 'function (doc) {\n if (doc.type && doc.type==\'userIngredientRequest\') {\n emit(doc.ingredient_name, 1);\n }\n}', 41 | 'reduce': '_sum' 42 | }, 43 | 'cuisines': { 44 | 'map': 'function (doc) {\n if (doc.type && doc.type==\'userCuisineRequest\') {\n emit(doc.cuisine_name, 1);\n }\n}', 45 | 'reduce': '_sum' 46 | }, 47 | 'recipes': { 48 | 'map': 'function (doc) {\n if (doc.type && doc.type==\'userRecipeRequest\') {\n emit(doc.recipe_title, 1);\n }\n}', 49 | 'reduce': '_sum' 50 | } 51 | }, 52 | 'language': 'javascript' 53 | } 54 | db.create_document(design_doc) 55 | # see if the by_day_of_week design doc exists, if not then create it 56 | query = Query(db, selector={ '_id': '_design/by_day_of_week' }) 57 | result = query()['docs'] 58 | if result is None or len(result) <= 0: 59 | design_doc = { 60 | '_id': '_design/by_day_of_week', 61 | 'views': { 62 | 'ingredients': { 63 | 'map': 'function (doc) {\n if (doc.type && doc.type==\'userIngredientRequest\') {\n var weekdays = [\'Sunday\',\'Monday\',\'Tuesday\',\'Wednesday\',\'Thursday\',\'Friday\',\'Saturday\'];\n emit(weekdays[new Date(doc.date).getDay()], 1);\n }\n}', 64 | 'reduce': '_sum' 65 | }, 66 | 'cuisines': { 67 | 'map': 'function (doc) {\n if (doc.type && doc.type==\'userCuisineRequest\') {\n var weekdays = [\'Sunday\',\'Monday\',\'Tuesday\',\'Wednesday\',\'Thursday\',\'Friday\',\'Saturday\'];\n emit(weekdays[new Date(doc.date).getDay()], 1);\n }\n}', 68 | 'reduce': '_sum' 69 | }, 70 | 'recipes': { 71 | 'map': 'function (doc) {\n if (doc.type && doc.type==\'userRecipeRequest\') {\n var weekdays = [\'Sunday\',\'Monday\',\'Tuesday\',\'Wednesday\',\'Thursday\',\'Friday\',\'Saturday\'];\n emit(weekdays[new Date(doc.date).getDay()], 1);\n }\n}', 72 | 'reduce': '_sum' 73 | } 74 | }, 75 | 'language': 'javascript' 76 | } 77 | db.create_document(design_doc) 78 | finally: 79 | self.client.disconnect() 80 | 81 | # User 82 | 83 | def add_user(self, user_id): 84 | """ 85 | Adds a new user to Cloudant if a user with the specified ID does not already exist. 86 | Parameters 87 | ---------- 88 | user_id - The ID of the user (typically the ID returned from Slack) 89 | """ 90 | user_doc = { 91 | 'type': 'user', 92 | 'name': user_id 93 | } 94 | return self.add_doc_if_not_exists(user_doc, 'name') 95 | 96 | # Ingredients 97 | 98 | @staticmethod 99 | def get_unique_ingredients_name(ingredient_str): 100 | """ 101 | Gets the unique name for the ingredient to be stored in Cloudant. 102 | Parameters 103 | ---------- 104 | ingredient_str - The ingredient or comma-separated list of ingredients specified by the user 105 | """ 106 | ingredients = [x.strip() for x in ingredient_str.lower().strip().split(',')] 107 | ingredients.sort() 108 | return ','.join([x for x in ingredients]) 109 | 110 | def find_ingredient(self, ingredient_str): 111 | """ 112 | Finds the ingredient based on the specified ingredientsStr in Cloudant. 113 | Parameters 114 | ---------- 115 | ingredient_str - The ingredient or comma-separated list of ingredients specified by the user 116 | """ 117 | return self.find_doc('ingredient', 'name', self.get_unique_ingredients_name(ingredient_str)) 118 | 119 | def add_ingredient(self, ingredient_str, matching_recipes, user_doc): 120 | """ 121 | Adds a new ingredient to Cloudant if an ingredient based on the specified ingredientsStr does not already exist. 122 | Parameters 123 | ---------- 124 | ingredient_str - The ingredient or comma-separated list of ingredients specified by the user 125 | matching_recipes - The recipes that match the specified ingredientsStr 126 | user_doc - The existing Cloudant doc for the user 127 | """ 128 | ingredient_doc = { 129 | 'type': 'ingredient', 130 | 'name': self.get_unique_ingredients_name(ingredient_str), 131 | 'recipes': matching_recipes 132 | } 133 | ingredient_doc = self.add_doc_if_not_exists(ingredient_doc, 'name') 134 | self.record_ingredient_request_for_user(ingredient_doc, user_doc) 135 | return ingredient_doc 136 | 137 | def record_ingredient_request_for_user(self, ingredient_doc, user_doc): 138 | """ 139 | Records the request by the user for the specified ingredient. 140 | Stores the ingredient and the number of times it has been accessed in the user doc. 141 | Parameters 142 | ---------- 143 | ingredient_doc - The existing Cloudant doc for the ingredient 144 | user_doc - The existing Cloudant doc for the user 145 | """ 146 | try: 147 | self.client.connect() 148 | # get latest user 149 | latest_user_doc = self.client[self.db_name][user_doc['_id']] 150 | # see if user has an array of ingredients, if not create it 151 | if 'ingredients' not in latest_user_doc.keys(): 152 | latest_user_doc['ingredients'] = [] 153 | # find the ingredient that matches the name of the passed in ingredient 154 | # if it doesn't exist create it 155 | user_ingredients = list(filter(lambda x: x['name'] == ingredient_doc['name'], latest_user_doc['ingredients'])) 156 | if len(user_ingredients) > 0: 157 | user_ingredient = user_ingredients[0] 158 | else: 159 | user_ingredient = {'name': ingredient_doc['name']} 160 | latest_user_doc['ingredients'].append(user_ingredient) 161 | # see if the user_ingredient exists, if not create it 162 | if 'count' not in user_ingredient.keys(): 163 | user_ingredient['count'] = 0 164 | # increment the count on the user_ingredient 165 | user_ingredient['count'] += 1 166 | # save the user doc 167 | latest_user_doc.save() 168 | # add a new doc with the user/ingredient details 169 | user_ingredient_doc = { 170 | 'type': 'userIngredientRequest', 171 | 'user_id': user_doc['_id'], 172 | 'user_name': user_doc['name'], 173 | 'ingredient_id': ingredient_doc['_id'], 174 | 'ingredient_name': ingredient_doc['name'], 175 | 'date': int(time.time()*1000) 176 | } 177 | db = self.client[self.db_name] 178 | db.create_document(user_ingredient_doc) 179 | finally: 180 | self.client.disconnect() 181 | 182 | # Cuisine 183 | 184 | @staticmethod 185 | def get_unique_cuisine_name(cuisine): 186 | """ 187 | Gets the unique name for the cuisine to be stored in Cloudant. 188 | Parameters 189 | ---------- 190 | cuisine - The cuisine specified by the user 191 | """ 192 | return cuisine.strip().lower() 193 | 194 | def find_cuisine(self, cuisine): 195 | """ 196 | Finds the cuisine with the specified name in Cloudant. 197 | Parameters 198 | ---------- 199 | cuisine - The cuisine specified by the user 200 | """ 201 | return self.find_doc('cuisine', 'name', self.get_unique_cuisine_name(cuisine)) 202 | 203 | def add_cuisine(self, cuisine_str, matching_recipes, user_doc): 204 | """ 205 | Adds a new cuisine to Cloudant if a cuisine with the specified name does not already exist. 206 | Parameters 207 | ---------- 208 | cuisine - The cuisine specified by the user 209 | matching_recipes - The recipes that match the specified cuisine 210 | user_doc - The existing Cloudant doc for the user 211 | """ 212 | cuisine_doc = { 213 | 'type': 'cuisine', 214 | 'name': self.get_unique_cuisine_name(cuisine_str), 215 | 'recipes': matching_recipes 216 | } 217 | cuisine_doc = self.add_doc_if_not_exists(cuisine_doc, 'name') 218 | self.record_cuisine_request_for_user(cuisine_doc, user_doc) 219 | return cuisine_doc 220 | 221 | def record_cuisine_request_for_user(self, cuisine_doc, user_doc): 222 | """ 223 | Records the request by the user for the specified cuisine. 224 | Stores the cuisine and the number of times it has been accessed in the user doc. 225 | Parameters 226 | ---------- 227 | cuisine_doc - The existing Cloudant doc for the cuisine 228 | user_doc - The existing Cloudant doc for the user 229 | """ 230 | try: 231 | self.client.connect() 232 | # get latest user 233 | latest_user_doc = self.client[self.db_name][user_doc['_id']] 234 | # see if user has an array of cuisines, if not create it 235 | if 'cuisines' not in latest_user_doc.keys(): 236 | latest_user_doc['cuisines'] = [] 237 | # find the cuisine that matches the name of the passed in cuisine 238 | # if it doesn't exist create it 239 | user_cuisines = list(filter(lambda x: x['name'] == cuisine_doc['name'], latest_user_doc['cuisines'])) 240 | if len(user_cuisines) > 0: 241 | user_cuisine = user_cuisines[0] 242 | else: 243 | user_cuisine = {'name': cuisine_doc['name']} 244 | latest_user_doc['cuisines'].append(user_cuisine) 245 | # see if the user_cuisine exists, if not create it 246 | if 'count' not in user_cuisine.keys(): 247 | user_cuisine['count'] = 0 248 | # increment the count on the user_cuisine 249 | user_cuisine['count'] += 1 250 | # save the user doc 251 | latest_user_doc.save() 252 | # add a new doc with the user/cuisine details 253 | user_cuisine_doc = { 254 | 'type': 'userCuisineRequest', 255 | 'user_id': user_doc['_id'], 256 | 'user_name': user_doc['name'], 257 | 'cuisine_id': cuisine_doc['_id'], 258 | 'cuisine_name': cuisine_doc['name'], 259 | 'date': int(time.time()*1000) 260 | } 261 | db = self.client[self.db_name] 262 | db.create_document(user_cuisine_doc) 263 | finally: 264 | self.client.disconnect() 265 | 266 | # Recipe 267 | 268 | @staticmethod 269 | def get_unique_recipe_name(recipe_id): 270 | """ 271 | Gets the unique name for the recipe to be stored in Cloudant. 272 | Parameters 273 | ---------- 274 | recipe_id - The ID of the recipe (typically the ID of the recipe returned from Spoonacular) 275 | """ 276 | return str(recipe_id).strip().lower() 277 | 278 | def find_recipe(self, recipe_id): 279 | """ 280 | Finds the recipe with the specified ID in Cloudant. 281 | Parameters 282 | ---------- 283 | recipe_id - The ID of the recipe (typically the ID of the recipe returned from Spoonacular) 284 | """ 285 | return self.find_doc('recipe', 'name', self.get_unique_recipe_name(recipe_id)) 286 | 287 | def find_favorite_recipes_for_user(self, user_doc, count): 288 | """ 289 | Finds the user's favorite recipes in Cloudant. 290 | Parameters 291 | ---------- 292 | user_doc - The existing Cloudant doc for the user 293 | count - The max number of recipes to return 294 | """ 295 | try: 296 | self.client.connect() 297 | db = self.client[self.db_name] 298 | latest_user_doc = db[user_doc['_id']] 299 | if 'recipes' in latest_user_doc.keys(): 300 | user_recipes = latest_user_doc['recipes'] 301 | user_recipes.sort(key=lambda x: x['count'], reverse=True) 302 | recipes = [] 303 | for i, recipe in enumerate(user_recipes): 304 | if i >= count: 305 | break 306 | recipes.append(recipe) 307 | return recipes 308 | else: 309 | return [] 310 | finally: 311 | self.client.disconnect() 312 | 313 | def add_recipe(self, recipe_id, recipe_title, recipe_detail, ingredient_cuisine_doc, user_doc): 314 | """ 315 | Adds a new recipe to Cloudant if a recipe with the specified name does not already exist. 316 | Parameters 317 | ---------- 318 | recipe_id - The ID of the recipe (typically the ID of the recipe returned from Spoonacular) 319 | recipe_title - The title of the recipe 320 | recipe_detail - The detailed instructions for making the recipe 321 | ingredient_cuisine_doc - The existing Cloudant doc for either the ingredient or cuisine selected before the recipe 322 | user_doc - The existing Cloudant doc for the user 323 | """ 324 | recipe = { 325 | 'type': 'recipe', 326 | 'name': self.get_unique_recipe_name(recipe_id), 327 | 'title': recipe_title.strip(), 328 | 'instructions': recipe_detail 329 | } 330 | recipe = self.add_doc_if_not_exists(recipe, 'name') 331 | self.record_recipe_request_for_user(recipe, ingredient_cuisine_doc, user_doc) 332 | return recipe 333 | 334 | def record_recipe_request_for_user(self, recipe_doc, ingredient_cuisine_doc, user_doc): 335 | """ 336 | Records the request by the user for the specified recipe. 337 | Stores the recipe and the number of times it has been accessed in the user doc. 338 | Parameters 339 | ---------- 340 | recipe_doc - The existing Cloudant doc for the recipe 341 | ingredient_cuisine_doc - The existing Cloudant doc for either the ingredient or cuisine selected before the recipe 342 | user_doc - The existing Cloudant doc for the user 343 | """ 344 | try: 345 | self.client.connect() 346 | # get latest user 347 | latest_user_doc = self.client[self.db_name][user_doc['_id']] 348 | # see if user has an array of recipes, if not create it 349 | if 'recipes' not in latest_user_doc.keys(): 350 | latest_user_doc['recipes'] = [] 351 | # find the recipe that matches the name of the passed in recipe 352 | # if it doesn't exist create it 353 | user_recipes = list(filter(lambda x: x['id'] == recipe_doc['name'], latest_user_doc['recipes'])) 354 | if len(user_recipes) > 0: 355 | user_recipe = user_recipes[0] 356 | else: 357 | user_recipe = { 358 | 'id': recipe_doc['name'], 359 | 'title': recipe_doc['title'] 360 | } 361 | latest_user_doc['recipes'].append(user_recipe) 362 | # see if the user_recipe exists, if not create it 363 | if 'count' not in user_recipe.keys(): 364 | user_recipe['count'] = 0 365 | # increment the count on the user_recipe 366 | user_recipe['count'] += 1 367 | # save the user doc 368 | latest_user_doc.save() 369 | # add a new doc with the user/recipe details 370 | user_recipe_doc = { 371 | 'type': 'userRecipeRequest', 372 | 'user_id': user_doc['_id'], 373 | 'user_name': user_doc['name'], 374 | 'recipe_id': recipe_doc['_id'], 375 | 'recipe_title': recipe_doc['title'], 376 | 'date': int(time.time()*1000) 377 | } 378 | db = self.client[self.db_name] 379 | db.create_document(user_recipe_doc) 380 | finally: 381 | self.client.disconnect() 382 | 383 | # Cloudant Helper Methods 384 | 385 | def find_doc(self, doc_type, property_name, property_value): 386 | """ 387 | Finds a doc based on the specified doc_type, property_name, and property_value. 388 | Parameters 389 | ---------- 390 | doc_type - The type value of the document stored in Cloudant 391 | property_name - The property name to search for 392 | property_value - The value that should match for the specified property name 393 | """ 394 | try: 395 | self.client.connect() 396 | db = self.client[self.db_name] 397 | selector = { 398 | '_id': {'$gt': 0}, 399 | 'type': doc_type, 400 | property_name: property_value 401 | } 402 | query = Query(db, selector=selector) 403 | for doc in query()['docs']: 404 | return doc 405 | return None 406 | finally: 407 | self.client.disconnect() 408 | 409 | def add_doc_if_not_exists(self, doc, unique_property_name): 410 | """ 411 | Adds a new doc to Cloudant if a doc with the same value for unique_property_name does not exist. 412 | Parameters 413 | ---------- 414 | doc - The document to add 415 | unique_property_name - The name of the property used to search for an existing document (the value will be extracted from the doc provided) 416 | """ 417 | doc_type = doc['type'] 418 | property_value = doc[unique_property_name] 419 | existing_doc = self.find_doc(doc_type, unique_property_name, property_value) 420 | if existing_doc is not None: 421 | print('Returning {} doc where {}={}'.format(doc_type, unique_property_name, property_value)) 422 | return existing_doc 423 | else: 424 | print('Creating {} doc where {}={}'.format(doc_type, unique_property_name, property_value)) 425 | try: 426 | self.client.connect() 427 | db = self.client[self.db_name] 428 | return db.create_document(doc) 429 | finally: 430 | self.client.disconnect() 431 | --------------------------------------------------------------------------------