16 | Thanks for creating a Python Starter Application.
17 |
18 |
19 |
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 | 
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 | 
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 | 
147 |
148 | 3. Click the appropriate button to gain access (i.e. **Get Regular Access**)
149 |
150 | 
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 | 
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 | 
177 |
178 | 2. In the IBM Bluemix Catalog search for **Watson Conversation**.
179 | 3. Select the **Conversation** service.
180 |
181 | 
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 | 
187 |
188 | 6. Find your newly created Credentials and click **View Credentials**
189 |
190 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
292 |
293 | 4. Add each environment variable from your .env file and click the **Save** button:
294 |
295 | 
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 | 
302 |
303 | Here are some sample conversations you can have with sous-chef:
304 |
305 | 
306 |
307 | 
308 |
309 | 
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 |
--------------------------------------------------------------------------------