diff --git a/.gitignore b/.gitignore index 70f5d8d35a..0205d62f17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,2 @@ -### Python template -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Created by .ignore support plugin (hsz.mobi) +*.pyc +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..3e0558e07f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# How to become a contributor and submit your own code + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA] + (https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA] + (https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the sample to which + you are contributing. Refer to the + [Google Cloud Platform Samples Style Guide] + (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the + recommended coding standards for this organization. +1. Ensure that your code has an appropriate set of unit tests which all pass. +1. Submit a pull request. + diff --git a/LICENSE b/LICENSE index 8dada3edaf..8405e89a0b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,191 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. + limitations under the License. \ No newline at end of file diff --git a/Procfile b/Procfile deleted file mode 100644 index e6cb5adcaf..0000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python app.py \ No newline at end of file diff --git a/README.md b/README.md index 6d16b486bf..2a32c01731 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,38 @@ -# Api.ai - sample webhook implementation in Python +# Dialogflow Fulfillment Weather Sample Python (Flask) -This is a really simple webhook implementation that gets Api.ai classification JSON (i.e. a JSON output of Api.ai /query endpoint) and returns a fulfillment response. +## Setup Instructions -More info about Api.ai webhooks could be found here: -[Api.ai Webhook](https://docs.api.ai/docs/webhook) +### WWO Weather API Setup + 1. Get a WWO API key, by going to https://developer.worldweatheronline.com/api/ and following the instructions to get an API key that includes forecasts 14 days into the future + 1. Paste your API key for the value of the `WWO_API_KEY` varible on line 28 of `config.py` -# Deploy to: -[![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) +### Dialogflow Setup + 1. Create an account on Dialogflow + 1. Create a new Dialogflow agent + 1. Restore the `dialogflow-agent.zip` ZIP file in the root of this repo + 1. Go to your agent's settings and then the *Export and Import* tab + 1. Click the *Restore from ZIP* button + 1. Select the `dialogflow-agent.zip` ZIP file in the root of this repo + 1. Type *RESTORE* and and click the *Restore* button -# What does the service do? -It's a weather information fulfillment service that uses [Yahoo! Weather API](https://developer.yahoo.com/weather/). -The services takes the `geo-city` parameter from the action, performs geolocation for the city and requests weather information from Yahoo! Weather public API. +### Fulfillment Setup + 1. Deploy fulfillment to App Engine + 1. [Download and authenticate the Google Cloud SDK](https://cloud.google.com/sdk/docs/quickstart-macos) + 1. Run `gcloud app deploy`, make a note of the service URL, which will be used in the next step + 1. Set the fulfillment URL in Dialogflow to your App Engine service URL + 1. Go to your [agent's fulfillment page](https://console.dialogflow.com/api-client/#/agent//fulfillment) + 1. Click the switch to enable webhook for your agent + 1. Enter you App Engine service URL (e.g. `https://weather-10929.appspot.com/`) to the URL field + 1. Click *Save* at the bottom of the page -The service packs the result in the Api.ai webhook-compatible response JSON and returns it to Api.ai. +## How to report bugs +* If you find any issues, please open a bug here on GitHub +## How to make contributions? +Please read and follow the steps in the CONTRIBUTING.md + +## License +See LICENSE.md + +## Terms +Your use of this sample is subject to, and by using or downloading the sample files you agree to comply with, the [Google APIs Terms of Service](https://developers.google.com/terms/) and the [Dialogflow's Terms of Use and Privacy Policy](https://dialogflow.com/terms/). \ No newline at end of file diff --git a/app.json b/app.json deleted file mode 100644 index 768cd2eae5..0000000000 --- a/app.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "API.AI webhook demonstration", - "description": "API.AI webhook demonstration", - "repository": "https://github.com/api-ai/apiai-weather-webhook-sample", - "logo": "http://xvir.github.io/img/apiai.png", - "keywords": ["api.ai", "natural language"] -} diff --git a/app.py b/app.py deleted file mode 100755 index 784d9e571b..0000000000 --- a/app.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python - -import urllib -import json -import os - -from flask import Flask -from flask import request -from flask import make_response - -# Flask app should start in global layout -app = Flask(__name__) - - -@app.route('/webhook', methods=['POST']) -def webhook(): - req = request.get_json(silent=True, force=True) - - print("Request:") - print(json.dumps(req, indent=4)) - - res = processRequest(req) - - res = json.dumps(res, indent=4) - # print(res) - r = make_response(res) - r.headers['Content-Type'] = 'application/json' - return r - - -def processRequest(req): - if req.get("result").get("action") != "yahooWeatherForecast": - return {} - baseurl = "https://query.yahooapis.com/v1/public/yql?" - yql_query = makeYqlQuery(req) - if yql_query is None: - return {} - yql_url = baseurl + urllib.urlencode({'q': yql_query}) + "&format=json" - result = urllib.urlopen(yql_url).read() - data = json.loads(result) - res = makeWebhookResult(data) - return res - - -def makeYqlQuery(req): - result = req.get("result") - parameters = result.get("parameters") - city = parameters.get("geo-city") - if city is None: - return None - - return "select * from weather.forecast where woeid in (select woeid from geo.places(1) where text='" + city + "')" - - -def makeWebhookResult(data): - query = data.get('query') - if query is None: - return {} - - result = query.get('results') - if result is None: - return {} - - channel = result.get('channel') - if channel is None: - return {} - - item = channel.get('item') - location = channel.get('location') - units = channel.get('units') - if (location is None) or (item is None) or (units is None): - return {} - - condition = item.get('condition') - if condition is None: - return {} - - # print(json.dumps(item, indent=4)) - - speech = "Today in " + location.get('city') + ": " + condition.get('text') + \ - ", the temperature is " + condition.get('temp') + " " + units.get('temperature') - - print("Response:") - print(speech) - - return { - "speech": speech, - "displayText": speech, - # "data": data, - # "contextOut": [], - "source": "apiai-weather-webhook-sample" - } - - -if __name__ == '__main__': - port = int(os.getenv('PORT', 5000)) - - print "Starting app on port %d" % port - - app.run(debug=False, port=port, host='0.0.0.0') diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000000..19f5a56ebf --- /dev/null +++ b/app.yaml @@ -0,0 +1,17 @@ +runtime: python +env: flex +entrypoint: gunicorn -b :$PORT main:app + +runtime_config: + python_version: 3 + +# This sample incurs costs to run on the App Engine flexible environment. +# The settings below are to reduce costs during testing and are not appropriate +# for production use. For more information, see: +# https://cloud.google.com/appengine/docs/flexible/python/configuring-your-app-with-app-yaml +manual_scaling: + instances: 1 +resources: + cpu: 1 + memory_gb: 0.5 + disk_size_gb: 10 \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000000..02b2295e62 --- /dev/null +++ b/config.py @@ -0,0 +1,37 @@ +# -*- coding:utf8 -*- +# !/usr/bin/env python +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module that holds the configuration for app.py including +API keys, temperature and forecast defaults and limits + +This is meant to be used with the sample weather agent for Dialogflow, located at +https://console.dialogflow.com/api-client/#/agent//prebuiltAgents/Weather + +This requires setting the WWO_API_KEY constant in config.py to a string with +a valid WWO API key for retrieving weather up to 14 days in the future. Get an +WWO API key here: https://developer.worldweatheronline.com/api/ +""" + +WWO_API_KEY = '' +MAX_FORECAST_LEN = 13 +_DEFAULT_TEMP_UNIT = 'F' + +_TEMP_LIMITS = { + 'hot': {'C': 25, 'F': 77}, + 'warm': {'C': 15, 'F': 59}, + 'chilly': {'C': 15, 'F': 41}, + 'cold': {'C': -5, 'F': 23} +} diff --git a/dialogflow-agent.zip b/dialogflow-agent.zip new file mode 100644 index 0000000000..1afdca5ed9 Binary files /dev/null and b/dialogflow-agent.zip differ diff --git a/forecast.py b/forecast.py new file mode 100644 index 0000000000..dfc8f9c158 --- /dev/null +++ b/forecast.py @@ -0,0 +1,604 @@ +# -*- coding:utf8 -*- +# !/usr/bin/env python +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Module that defines the Forecast class and defines helper functions to +process and validate date related to the weather forecast class + +This is meant to be used with the sample weather agent for Dialogflow, located at +https://console.dialogflow.com/api-client/#/agent//prebuiltAgents/Weather + +This sample uses the WWO Weather Forecast API and requires an WWO API key +Get a WWO API key here: https://developer.worldweatheronline.com/api/ +""" + +import random +from datetime import datetime as dt +from datetime import timedelta + +import requests + +from config import (_TEMP_LIMITS, _DEFAULT_TEMP_UNIT, WWO_API_KEY, + MAX_FORECAST_LEN) +from weather_responses import ( + LIST_YES, + LIST_NO, + LIST_COLD, + LIST_CHILLY, + LIST_WARM, + LIST_HOT, + WEATHER_CURRENT, + WEATHER_DATE, + WEATHER_WEEKDAY, + WEATHER_DATE_TIME, + WEATHER_TIME_PERIOD, + WEATHER_TIME_PERIOD_DEFINED, + WEATHER_DATE_PERIOD_WEEKEND, + WEATHER_DATE_PERIOD, + WEATHER_ACTIVITY_YES, + WEATHER_ACTIVITY_NO, + RESPONSE_WEATHER_CONDITION, + RESPONSE_WEATHER_OUTFIT) +from weather_entities import (WINTER_ACTIVITY, SUMMER_ACTIVITY, DEMI_ACTIVITY, + CONDITION_DICT, UNSUPPORTED, COLD_WEATHER, + WARM_WEATHER, HOT_WEATHER, RAIN, SNOW, SUN) + + +class Forecast(object): + """The Forecast object implements tracking of and forecast retrieval for + a request for a weather forecast. Several methods return various human + readable strings that contain the weather forecast, condition, temperature + and the appropriateness of outfits and activities to for forecasted weather + + This requires setting the WWO_API_KEY constant in config.py to a string + with a valid WWO API key for retrieving weather forecasts + + Attributes: + city (str): the city for the weather forecast + datetime_start (datetime.datetime): forecast start date or datetime + datetime_end (datetime.datetime): forecast end date or datetime + unit (str): the unit of temperature: Celsius ('C') or Fahrenheit ('F') + action (dict): any actions in the request (activity, condition, outfit) + forecast (dict): structure containing the weather forecast from WWO + """ + + def __init__(self, params): + """Initializes the Forecast object + + gets the forecast for the provided dates + """ + + self.city = params['city'] + self.datetime_start = params['datetime_start'] + self.datetime_end = params['datetime_end'] + self.unit = params['unit'] + self.action = { + 'activity': params['activity'], + 'condition': params['condition'], + 'outfit': params['outfit'], + } + + self.forecast = self.__get_forecast() + + def __get_forecast(self): + """Takes a date or date period and a city + + raises an exception when dates are outside what can be forecasted + Returns the weather for the period and city as a dict + """ + + datetime_start = self.datetime_start + datetime_end = self.datetime_end + + if datetime_start and datetime_end: + date_interval = datetime_end - datetime_start + forecast_length = date_interval.days + elif datetime_start: + forecast_length = 1 + else: + datetime_start = dt.now().date() + forecast_length = 1 + + # Get the start date + try: + date_start = datetime_start.date() + except AttributeError: + date_start = datetime_start + + # Get the furthest date in the future we can get a forecast for + max_forecast_date = dt.now().date() + timedelta(days=MAX_FORECAST_LEN) + furthest_date_requested = date_start + timedelta(days=forecast_length) + + # Check to see that the forecast dates requested are not too far into + # the future + if furthest_date_requested > max_forecast_date: + raise ValueError( + 'I couldn\'t find a forecast for that far in the future.') + + # Get the weather for each day (each needs a separate API call) + for day in range(forecast_length): + current_date = date_start + timedelta(days=day) + + response = self.__call_wwo_api(current_date.strftime('%Y-%m-%d')) + + try: + forecast['weather'].append(response['weather'][0]) + except NameError: + forecast = response + + return forecast + + def __call_wwo_api(self, date): + """Calls the wwo weather API for a date + + raises an exception for network errors + Returns a dict of the JSON 'data' attribute in the response + """ + + wwo_data = { + 'key': WWO_API_KEY, + 'q': self.city, + 'format': 'json', + 'num_of_days': 1, + 'mca': 'no', + 'lang': 'en', + 'cc': 'yes', + 'tp': '1', + 'fx': 'yes', + 'date': date + } + + response = requests.get( + 'http://api.worldweatheronline.com/premium/v1/weather.ashx', + params=wwo_data + ) + + weather_data = response.json()['data'] + error = weather_data.get('error') + if error: + raise IOError(error[0]['msg']) + else: + return weather_data + + def __get_max_min_temp(self): + """Calculate the max and min temperatures for the date range + """ + + temps = [] + for day in self.forecast['weather']: + for hour in day['hourly']: + temps.append(int(hour['temp' + self.unit])) + + return (max(temps), min(temps)) + + def get_datetime_response(self): + """Takes a datetime and forecast + + Returns the forecast for that datetime as a string + """ + + max_temp = int(self.forecast['weather'][0]['maxtemp' + self.unit]) + min_temp = int(self.forecast['weather'][0]['mintemp' + self.unit]) + temp = (max_temp + min_temp) / 2 + temperature = str(temp).encode('utf-8') + \ + u'°'.encode('utf-8') + self.unit.encode('utf-8') + condition = self.forecast['weather'][0]['hourly'][ + 12]['weatherDesc'][0]['value'].lower() + + # Get the start date + try: + date_start = self.datetime_start.date() + except AttributeError: + date_start = self.datetime_start + + # if the weather forecast is for today and they specified a time + if (date_start == dt.now().date() and + isinstance(self.datetime_start, dt)): + output_string = random.choice(WEATHER_DATE_TIME) + response = output_string.format( + place=self.city, + time=self.datetime_start.strftime('%I:%M%p'), + temperature=temperature, + condition=condition, + day='Today') + # else + else: + # if it's within a week + time_difference = date_start - dt.now().date() + if time_difference <= timedelta(days=7): + # Get the day of the week or set to 'today' + day = self.datetime_start.strftime('%A') + if date_start == dt.now().date(): + day = 'Today' + # Format Response + output_string = random.choice(WEATHER_DATE) + response = output_string.format( + place=self.city, + day=day, + temperature=temperature, + condition=condition) + # if the date is more than a week away + else: + output_string = random.choice(WEATHER_WEEKDAY) + response = output_string.format( + place=self.city, + condition=condition, + temperature=temperature, + date=self.datetime_start.strftime('%B %-d')) + return response + + def get_datetime_period_response(self): + """Takes a date period and forecast + + Returns the forecast for the date period as a string + """ + + datetime_start = self.datetime_start + datetime_end = self.datetime_end + forecast = self.forecast + + # datetime period over the same day + if datetime_start.day == datetime_end.day: + + # Get the temperature throughout the time period and average it + temps = [] + hours = [] + # Get the set of hours in military time to average over for the day + for hour in range(datetime_start.hour, datetime_end.hour + 1): + hours.append(str(hour * 100)) # WWO API uses military time + # Get the forecasted temperature for every hour during the period + for hour in forecast['weather'][0]['hourly']: + if hour['time'] in hours: + temps.append(hour['temp' + self.unit]) + # Calculate the average temperature for the time period + avg_temp = sum(temps) / len(temps) + # Make a human readable string of the temperature + temperature = str(avg_temp) + u'°'.encode('utf-8') + self.unit + + # Get the conditions for the time period + condition = forecast['weather'][0]['hourly'][ + datetime_start.hour + 1]['weatherDesc'][0]['value'].lower() + + # Choose the right word to describe the time period + if datetime_start.hour <= 12 and datetime_end.hour <= 16: + time_period = 'afternoon' + elif datetime_start.hour <= 0 and datetime_end.hour <= 8: + time_period = 'night' + elif datetime_start.hour <= 16 and datetime_end.hour <= 23: + time_period = 'tonight' + elif datetime_start.hour <= 8 and datetime_end.hour <= 12: + time_period = 'morning' + + # if the time period can be described with a word use it here + if time_period: + output_string = random.choice( + WEATHER_TIME_PERIOD_DEFINED) + response = output_string.format( + place=self.city, + time_period=time_period, + temperature=temperature, + condition=condition) + + # If the time period cannot be defined by a single word use the + # time the user provided + else: + output_string = random.choice(WEATHER_TIME_PERIOD) + response = output_string.format( + condition=condition, + city=self.city, + temp=temperature, + time_start=datetime_start.strftime('%I:%M%p'), + time_end=datetime_end.strftime('%I:%M%p')) + + # datetime period over multiple days + else: + # If the user is requesting weather for the weekend + if datetime_start.day == 5 and datetime_end.day == 6: + response = random.choice(WEATHER_DATE_PERIOD_WEEKEND).format( + city=self.city, + condition_sun=forecast['weather'][1]['hourly'][ + 12]['weatherDesc'][0]['value'].lower(), + sun_temp_min=forecast['weather'][1]['mintemp' + self.unit], + sun_temp_max=forecast['weather'][1]['maxtemp' + self.unit], + condition_sat=forecast['weather'][0]['hourly'][ + 12]['weatherDesc'][0]['value'].lower(), + sat_temp_min=forecast['weather'][0]['mintemp' + self.unit], + sat_temp_max=forecast['weather'][0]['maxtemp' + self.unit]) + # If the user is requesting a non-weekend date range + else: + (max_temp, min_temp) = self.__get_max_min_temp() + + # Format temperature strings + max_temp = str(max_temp).encode('utf-8') + \ + u'°'.encode('utf-8') + self.unit.encode('utf-8') + min_temp = str(min_temp).encode('utf-8') + \ + u'°'.encode('utf-8') + self.unit.encode('utf-8') + + response = random.choice(WEATHER_DATE_PERIOD).format( + date_start=datetime_start.strftime('%Y-%m-%d'), + date_end=datetime_end.strftime('%Y-%m-%d'), + city=self.city, + condition=forecast['weather'][0]['hourly'][ + 12]['weatherDesc'][0]['value'].lower(), + degree_list_min=min_temp, + degree_list_max=max_temp) + + return response + + def get_activity_response(self): + """Takes an activity and a forecast + + returns the appropriateness of activity with the weather as a string + """ + + activity = self.action['activity'] + (max_temp, _) = self.__get_max_min_temp() + + if activity in DEMI_ACTIVITY: + resp = random.choice(WEATHER_ACTIVITY_YES).format( + activity=activity) + elif activity in WINTER_ACTIVITY: + if max_temp <= _TEMP_LIMITS['cold'][self.unit]: + resp = random.choice(WEATHER_ACTIVITY_YES).format( + activity=activity) + else: + resp = random.choice(WEATHER_ACTIVITY_NO).format( + activity=activity) + elif activity in SUMMER_ACTIVITY: + if max_temp >= _TEMP_LIMITS['warm'][self.unit]: + resp = random.choice(WEATHER_ACTIVITY_YES).format( + activity=activity) + else: + resp = random.choice(WEATHER_ACTIVITY_NO).format( + activity=activity) + else: + resp = 'I don\'t know about %s' % activity + + return resp + + def get_condition_response(self): + """Takes a condition and returns the probability as a string + """ + + condition = self.action['condition'] + + if condition in CONDITION_DICT.keys(): + condition_chance = self.forecast['weather'][ + 0]['hourly'][12][CONDITION_DICT[condition]] + resp = random.choice(RESPONSE_WEATHER_CONDITION).format( + condition_original=condition, + condition=condition_chance + ) + else: + resp = 'I don\'t know about %s' % condition + + return resp + + def get_outfit_response(self): + """Takes an outfit and a forecast + + returns the appropriateness of outfit with the weather as a string + """ + + outfit = self.action['outfit'] + condition = self.action['condition'] + (max_temp, min_temp) = self.__get_max_min_temp() + condition_chance = None + + if outfit in COLD_WEATHER: + answer = LIST_YES if min_temp < _TEMP_LIMITS[ + 'chilly'][self.unit] else LIST_NO + elif outfit in WARM_WEATHER: + answer = LIST_YES if max_temp < _TEMP_LIMITS[ + 'warm'][self.unit] else LIST_NO + elif outfit in HOT_WEATHER: + answer = LIST_YES if max_temp < _TEMP_LIMITS[ + 'hot'][self.unit] else LIST_NO + elif outfit in RAIN: + condition = 'rain' + condition_chance = self.forecast['weather'][ + 0]['hourly'][12]['chanceofrain'] + answer = LIST_YES if condition_chance < 50 else LIST_NO + elif outfit in SNOW: + condition = 'snow' + condition_chance = self.forecast['weather'][ + 0]['hourly'][12]['chanceofsnow'] + answer = LIST_YES if condition_chance < 50 else LIST_NO + elif outfit in SUN: + condition = 'sunshine' + condition_chance = self.forecast['weather'][ + 0]['hourly'][12]['chanceofsunshine'] + answer = LIST_YES if condition_chance > 50 else LIST_NO + else: + return 'I don\'t know about %s' % outfit + + if condition_chance: + return random.choice(RESPONSE_WEATHER_OUTFIT).format( + condition_original=condition, + condition=condition_chance, + answer=random.choice(answer)) + else: + return random.choice(answer) + + def get_temperature_response(self): + """Takes a temperature and indicates its severity in a string + """ + + temp = int(self.forecast['current_condition'][0]['temp_' + self.unit]) + + if temp >= _TEMP_LIMITS['hot'][self.unit]: + resp = LIST_HOT + elif temp > _TEMP_LIMITS['chilly'][self.unit]: + resp = LIST_WARM + elif temp > _TEMP_LIMITS['cold'][self.unit]: + resp = LIST_CHILLY + else: + resp = LIST_COLD + + return random.choice(resp) + + def get_current_response(self): + """Takes a forecast and returns the current conditions as a string + """ + + # Get the temperature by average the high and low for the day + temp = self.forecast['current_condition'][0]['temp_' + self.unit] + temperature = temp.encode( + 'utf-8') + u'°'.encode('utf-8') + self.unit.encode('utf-8') + + # Get the conditions in the middle of the day + condition = self.forecast['weather'][0][ + 'hourly'][12]['weatherDesc'][0]['value'] + + output_string = random.choice(WEATHER_CURRENT) + + return output_string.format( + place=self.city, + temperature=temperature, + condition=condition + ) + + +def validate_params(parameters): + """Takes a list of parameters from a HTTP request and validates them + + Returns a string of errors (or empty string) and a list of params + """ + + # Initialize error and params + error_response = '' + params = {} + + # City + if (parameters.get('address') and + isinstance(parameters.get('address'), dict)): + params['city'] = parameters.get('address').get('city') + else: + params['city'] = None + error_response += 'please specify city ' + + # Date-time and date-periods + if parameters.get('date-time') or parameters.get('date-period'): + # Get the date time or date period (can't be both) + if parameters.get('date-time'): + datetime_input = parameters.get('date-time') + else: + datetime_input = parameters.get('date-period') + + datetime_start, datetime_end = parse_datetime_input(datetime_input) + params['datetime_start'] = datetime_start + params['datetime_end'] = datetime_end + + # Unit + params['unit'] = parameters.get('unit') + if not params['unit'] and _DEFAULT_TEMP_UNIT: + params['unit'] = _DEFAULT_TEMP_UNIT + + # activity + if parameters.get('activity'): + activity = parameters.get('activity') + if (activity not in SUMMER_ACTIVITY and + activity not in WINTER_ACTIVITY and + activity not in DEMI_ACTIVITY): + error_response += 'unknown activity ' + params['activity'] = parameters.get('activity') + + # condition + params['condition'] = parameters.get('condition') + if params['condition'] in UNSUPPORTED: + error_response += 'unsupported condition ' + + # outfit + params['outfit'] = parameters.get('outfit') + + # Special parameters + # activity + params['activity'] = parameters.get('activity') + + # condition + params['condition'] = parameters.get('condition') + + return error_response.strip(), params + + +def parse_datetime_input(datetime_input): + """Takes a string containing date/time and intervals in ISO-8601 format + + Returns a start and end Python datetime.datetime object + datetimes are None if the string is not a date/time + datetime_end is None if the string is not a date/time interval + """ + + # Date time + # If the string is length 8 datetime_input has the form 17:30:00 + if len(datetime_input) == 8: + # if only the time is provided assume its for the current date + current_date = dt.now().strftime('%Y-%m-%dT') + + datetime_start = dt.strptime( + current_date + datetime_input, + '%Y-%m-%dT%H:%M:%S') + datetime_end = None + # If the string is length 10 datetime_input has the form 2014-08-09 + elif len(datetime_input) == 10: + datetime_start = dt.strptime(datetime_input, '%Y-%m-%d').date() + datetime_end = None + # If the string is length 20 datetime_input has the form + # 2014-08-09T16:30:00Z + elif len(datetime_input) == 20: + datetime_start = dt.strptime(datetime_input, '%Y-%m-%dT%H:%M:%SZ') + datetime_end = None + + # Date Periods + # If the string is length 17 datetime_input has the form + # 13:30:00/14:30:00 + elif len(datetime_input) == 17: + # if only the time is provided assume its for the current date + current_date = dt.now().strftime('%Y-%m-%dT') + + # Split date into start and end times + datetime_input_start = datetime_input.split('/')[0] + datetime_input_end = datetime_input.split('/')[1] + + datetime_start = dt.strptime( + current_date + datetime_input_start, '%Y-%m-%dT%H:%M:%S') + datetime_end = dt.strptime( + current_date + datetime_input_end, '%Y-%m-%dT%H:%M:%S') + # If the string is length 21 datetime_input has the form + # 2014-01-01/2014-12-31 + elif len(datetime_input) == 21: + # Split date into start and end times + datetime_input_start = datetime_input.split('/')[0] + datetime_input_end = datetime_input.split('/')[1] + + datetime_start = dt.strptime( + datetime_input_start, '%Y-%m-%d').date() + datetime_end = dt.strptime(datetime_input_end, '%Y-%m-%d').date() + # If the string is length 41 datetime_input has the form + # 2017-02-08T08:00:00Z/2017-02-08T12:00:00Z + elif len(datetime_input) == 41: + # Split date into start and end times + datetime_input_start = datetime_input.split('/')[0] + datetime_input_end = datetime_input.split('/')[1] + + datetime_start = dt.strptime( + datetime_input_start, '%Y-%m-%dT%H:%M:%SZ') + datetime_end = dt.strptime( + datetime_input_end, '%Y-%m-%dT%H:%M:%SZ') + else: + datetime_start = None + datetime_end = None + + return datetime_start, datetime_end diff --git a/main.py b/main.py new file mode 100644 index 0000000000..0d83ccdf01 --- /dev/null +++ b/main.py @@ -0,0 +1,225 @@ +# -*- coding:utf8 -*- +# !/usr/bin/env python +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This is a sample for a weather fulfillment webhook for an Dialogflow agent +This is meant to be used with the sample weather agent for Dialogflow, located at +https://console.dialogflow.com/api-client/#/agent//prebuiltAgents/Weather + +This sample uses the WWO Weather Forecast API and requires an WWO API key +Get a WWO API key here: https://developer.worldweatheronline.com/api/ +""" + +import json + +from flask import Flask, request, make_response, jsonify + +from forecast import Forecast, validate_params + +app = Flask(__name__) +log = app.logger + + +@app.route('/', methods=['POST']) +def webhook(): + """This method handles the http requests for the Dialogflow webhook + + This is meant to be used in conjunction with the weather Dialogflow agent + """ + req = request.get_json(silent=True, force=True) + try: + action = req.get('queryResult').get('action') + except AttributeError: + return 'json error' + + if action == 'weather': + res = weather(req) + elif action == 'weather.activity': + res = weather_activity(req) + elif action == 'weather.condition': + res = weather_condition(req) + elif action == 'weather.outfit': + res = weather_outfit(req) + elif action == 'weather.temperature': + res = weather_temperature(req) + else: + log.error('Unexpected action.') + + print('Action: ' + action) + print('Response: ' + res) + + return make_response(jsonify({'fulfillmentText': res})) + + +def weather(req): + """Returns a string containing text with a response to the user + with the weather forecast or a prompt for more information + + Takes the city for the forecast and (optional) dates + uses the template responses found in weather_responses.py as templates + """ + parameters = req['queryResult']['parameters'] + + print('Dialogflow Parameters:') + print(json.dumps(parameters, indent=4)) + + # validate request parameters, return an error if there are issues + error, forecast_params = validate_params(parameters) + if error: + return error + + # create a forecast object which retrieves the forecast from a external API + try: + forecast = Forecast(forecast_params) + # return an error if there is an error getting the forecast + except (ValueError, IOError) as error: + return error + + # If the user requests a datetime period (a date/time range), get the + # response + if forecast.datetime_start and forecast.datetime_end: + response = forecast.get_datetime_period_response() + # If the user requests a specific datetime, get the response + elif forecast.datetime_start: + response = forecast.get_datetime_response() + # If the user doesn't request a date in the request get current conditions + else: + response = forecast.get_current_response() + + return response + + +def weather_activity(req): + """Returns a string containing text with a response to the user + with a indication if the activity provided is appropriate for the + current weather or a prompt for more information + + Takes a city, activity and (optional) dates + uses the template responses found in weather_responses.py as templates + and the activities listed in weather_entities.py + """ + + # validate request parameters, return an error if there are issues + error, forecast_params = validate_params(req['queryResult']['parameters']) + if error: + return error + + # Check to make sure there is a activity, if not return an error + if not forecast_params['activity']: + return 'What activity were you thinking of doing?' + + # create a forecast object which retrieves the forecast from a external API + try: + forecast = Forecast(forecast_params) + # return an error if there is an error getting the forecast + except (ValueError, IOError) as error: + return error + + # get the response + return forecast.get_activity_response() + + +def weather_condition(req): + """Returns a string containing a human-readable response to the user + with the probability of the provided weather condition occurring + or a prompt for more information + + Takes a city, condition and (optional) dates + uses the template responses found in weather_responses.py as templates + and the conditions listed in weather_entities.py + """ + + # validate request parameters, return an error if there are issues + error, forecast_params = validate_params(req['queryResult']['parameters']) + if error: + return error + + # Check to make sure there is a activity, if not return an error + if not forecast_params['condition']: + return 'What weather condition would you like to check?' + + # create a forecast object which retrieves the forecast from a external API + try: + forecast = Forecast(forecast_params) + # return an error if there is an error getting the forecast + except (ValueError, IOError) as error: + return error + + # get the response + return forecast.get_condition_response() + + +def weather_outfit(req): + """Returns a string containing text with a response to the user + with a indication if the outfit provided is appropriate for the + current weather or a prompt for more information + + Takes a city, outfit and (optional) dates + uses the template responses found in weather_responses.py as templates + and the outfits listed in weather_entities.py + """ + + # validate request parameters, return an error if there are issues + error, forecast_params = validate_params(req['queryResult']['parameters']) + if error: + return error + + # Validate that there are the required parameters to retrieve a forecast + if not forecast_params['outfit']: + return 'What are you planning on wearing?' + + # create a forecast object which retrieves the forecast from a external API + try: + forecast = Forecast(forecast_params) + # return an error if there is an error getting the forecast + except (ValueError, IOError) as error: + return error + + return forecast.get_outfit_response() + + +def weather_temperature(req): + """Returns a string containing text with a response to the user + with a indication if temperature provided is consisting with the + current weather or a prompt for more information + + Takes a city, temperature and (optional) dates. Temperature ranges for + hot, cold, chilly and warm can be configured in config.py + uses the template responses found in weather_responses.py as templates + """ + + parameters = req['queryResult']['parameters'] + + # validate request parameters, return an error if there are issues + error, forecast_params = validate_params(parameters) + if error: + return error + + # If the user didn't specify a temperature, get the weather for them + if not forecast_params['temperature']: + return weather(req) + + # create a forecast object which retrieves the forecast from a external API + try: + forecast = Forecast(forecast_params) + # return an error if there is an error getting the forecast + except (ValueError, IOError) as error: + return error + + return forecast.get_temperature_response() + + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0') diff --git a/requirements.txt b/requirements.txt index 56eeb0c801..537d103c75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -Flask==0.10.1 \ No newline at end of file +Flask==0.12.2 +requests==2.18.4 +gunicorn==19.7.1 diff --git a/weather_entities.py b/weather_entities.py new file mode 100644 index 0000000000..6b0b11d902 --- /dev/null +++ b/weather_entities.py @@ -0,0 +1,183 @@ +# -*- coding:utf8 -*- +# !/usr/bin/env python +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module defines the entities that can be responded to for activities, +conditions and outfits. + +This is meant to be used with the sample weather agent for Dialogflow, located at +https://console.dialogflow.com/api-client/#/agent//prebuiltAgents/Weather +""" + +# activity +WINTER_ACTIVITY = [ + 'skiing', + 'snowboarding', + 'snowball fighting', + 'snowball fights' +] + +SUMMER_ACTIVITY = [ + 'cycling', + 'run', + 'swimming', + 'jogging', + 'hiking', + 'skating', + 'parasailing', + 'windsurfing', + 'mushroom hunting', + 'elephant safari', + 'kayaking', + 'mountain biking', + 'surfing', + 'frisbee', + 'camping' +] + +DEMI_ACTIVITY = [ + 'sightseeing', + 'birdwatching', + 'tree climbing', +] + +# conditions + +CONDITION_DICT = { + 'rain': 'chanceofrain', + 'snow': 'chanceofsnow', + 'wind': 'chanceofwindy', + 'sun': 'chanceofsunshine', + 'fog': 'chanceoffog', + 'foggy': 'chanceoffog', + 'thunderstorm': 'chanceofthunder', + 'overcast': 'chanceofovercast', + 'clouds': 'cloudcover', +} + +SUPPORTED = [ + 'rain', + 'snow', + 'wind', + 'sun', + 'fog', + 'thunderstorm', + 'overcast', + 'clouds', + 'foggy', +] + +UNSUPPORTED = [ + 'shower', + 'ice', + 'freezing rain', + 'rain snow', + 'haze', + 'smoke', +] + +# outfit +COLD_WEATHER = [ + 'wool socks', + 'wool cap', + 'turtleneck', + 'thermal pants', + 'sweatshirt', + 'sweatpants', + 'sweater', + 'snowboard pants', + 'ski pants', + 'shawls', + 'scarf', + 'jumper', + 'balaclava', + 'beanie', + 'boots', + 'cardigan', + 'fleece top', + 'gloves', + 'jacket' +] + +WARM_WEATHER = [ + 'tennis shoes', + 'lounge wear', + 'socks', + 'sneakers', + 'sleeve shirt', + 'casual shirt', + 'coat', + 'dress pants', + 'dress shirt', + 'dress', + 'gum boots', + 'hat', + 'hoodie', +] + +HOT_WEATHER = [ + 'tank top', + 't-shirt', + 'swimwear', + 'swim goggles', + 'sunscreen', + 'skirt', + 'shorts', + 'bathing suit', + 'bra', + 'capri', + 'flips flops', + 'pool shoes', + 'sandals', + 'slippers', +] + +UNKNOWN_WEATHER = [ + 'underwear', + 'tie', + 'neck gaiter', + 'pajama', + 'sleepwear', + 'suit', +] + +RAIN = [ + 'umbrella', + 'coat', + 'gum boots', + 'hat', + 'jacket', + 'rain coat', + 'rain jacket', + 'rain pants', +] + +SNOW = [ + 'gloves', + 'fleece top', + 'ski pants', + 'snowboard pants', +] + +SUN = [ + 'swimwear', + 'swim goggles', + 'bra', + 'bathing suit', + 'flips flops', + 'sandals', + 'sunglasses', + 'sunscreen', +] diff --git a/weather_responses.py b/weather_responses.py new file mode 100644 index 0000000000..5f702e14c6 --- /dev/null +++ b/weather_responses.py @@ -0,0 +1,134 @@ +# -*- coding:utf8 -*- +# !/usr/bin/env python +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""This module defines the text based template responses to be formatted +and sent to users with the proper data + +This is meant to be used with the sample weather agent for Dialogflow, located at +https://console.dialogflow.com/api-client/#/agent//prebuiltAgents/Weather +""" + +LIST_YES = [ + 'Better have it with you, just in case.', + 'It never hurts to be extra prepared.', + 'Better to have it and not need it than to need it and not have it.', + 'Considering the forecast, I\'m going to say yes.' +] + +LIST_NO = [ + 'No, you should be fine without it.', + 'I don\'t think that will be necessary.', + 'You can bring it if you like, but I doubt you\'ll need it.', + 'It seems pretty unlikely you\'ll need that.' +] + +LIST_COLD = [ + 'Quite cold there.', + 'Pretty freezing, I would say.', + 'Don\'t forget your gloves.' +] + +LIST_CHILLY = [ + 'Quite chilly.', + 'You\'ll need a jacket for sure.' +] + +LIST_WARM = [ + 'Temperature is okay.' +] + +LIST_HOT = [ + 'Oh, that\'s hot!', + 'You\'ll definitely need sunscreen.' +] + +WEATHER_CURRENT = [ + 'The temperature in {place} now is {temperature} and {condition}.', + 'Right now it\'s {temperature} and {condition} in {place}.', + 'It\'s currently {temperature} and {condition} in {place}.', + 'The temperature in {place} is {temperature} and {condition}.' +] + +WEATHER_DATE = [ + '{day} in {place} it will be around {temperature} and {condition}.', + '{day} in {place} you can expect it to be around {temperature} and \ + {condition}.', + '{day} in {place} you can expect {condition}, with temperature around \ + {temperature}.', + '{day} in {place} it will be {condition}, {temperature}.', +] + +WEATHER_WEEKDAY = [ + 'On {date} in {place} it will be {condition}, {temperature}.', + 'On {date} in {place} it\'s expected to be {condition}, {temperature}.', + 'The forecast for {date} in {place} is {condition}, {temperature}.', + '{date} in {place} is expected to be {condition}, {temperature}.' +] + +WEATHER_DATE_TIME = [ + '{day} in {place} at {time} it will be around {temperature} and \ + {condition}.', + '{day} in {place} at {time} you can expect it to be around {temperature} \ + and {condition}.', + '{day} in {place} at {time} you can expect {condition}, with the \ + temperature around {temperature}.', + '{day} in {place} at {time} it will be {condition}, {temperature}.', + 'At {time} on {day} in {place} it will be {temperature} and {condition}.' +] + +WEATHER_TIME_PERIOD = [ + 'It will be {condition} in {city} and around {temp} on period from \ + {time_start} till {time_end}.' +] + +WEATHER_TIME_PERIOD_DEFINED = [ + 'This {time_period} in {place} it will be {temperature} and {condition}.', + 'This {time_period} in {place} you can expect {condition}, with \ + temperature around {temperature}.', + 'Expect a {condition} {time_period} in {place}, with temperature around \ + {temperature}.', + 'It will be {condition} in {place} and around {temperature} this \ + {time_period}.', +] + +WEATHER_DATE_PERIOD_WEEKEND = [ + 'On Saturday in {city} it will be {condition_sat}, ' + 'with temperatures from {sat_temp_min} to {sat_temp_max}. ' + 'And Sunday should be {condition_sun}, ' + 'with a low of {sun_temp_min} and a high of {sun_temp_max}.' +] + +WEATHER_DATE_PERIOD = [ + 'During period from {date_start} till {date_end}' + ' in {city} you can expect {condition}, ' + 'with a low of {degree_list_min} and a high of {degree_list_max}.' +] + +WEATHER_ACTIVITY_YES = [ + 'What a nice weather for {activity}!' +] + +WEATHER_ACTIVITY_NO = [ + 'Not the best weather for {activity}.' +] + +RESPONSE_WEATHER_CONDITION = [ + 'Chance of {condition_original} is {condition} percent.' +] + +RESPONSE_WEATHER_OUTFIT = [ + 'Chance of {condition_original} is {condition} percent. {answer}' +]