From aff33d52c52f5130677a3b7935329ec0048f5491 Mon Sep 17 00:00:00 2001 From: Honkware <119620994+Honkware@users.noreply.github.com> Date: Tue, 28 Mar 2023 14:02:14 -0500 Subject: [PATCH] Add OpenWeatherMap API Tool (#2083) Added tool for OpenWeatherMap API --- .../tools/examples/openweathermap.ipynb | 128 ++++++++++++++++++ docs/modules/agents/tools/getting_started.md | 8 ++ langchain/tools/openweathermap/__init__.py | 1 + langchain/tools/openweathermap/tool.py | 29 ++++ langchain/utilities/__init__.py | 2 + langchain/utilities/openweathermap.py | 79 +++++++++++ poetry.lock | 56 +++++++- pyproject.toml | 2 +- .../integration_tests/test_openweathermap.py | 24 ++++ 9 files changed, 323 insertions(+), 6 deletions(-) create mode 100644 docs/modules/agents/tools/examples/openweathermap.ipynb create mode 100644 langchain/tools/openweathermap/__init__.py create mode 100644 langchain/tools/openweathermap/tool.py create mode 100644 langchain/utilities/openweathermap.py create mode 100644 tests/integration_tests/test_openweathermap.py diff --git a/docs/modules/agents/tools/examples/openweathermap.ipynb b/docs/modules/agents/tools/examples/openweathermap.ipynb new file mode 100644 index 00000000..637daa0f --- /dev/null +++ b/docs/modules/agents/tools/examples/openweathermap.ipynb @@ -0,0 +1,128 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "245a954a", + "metadata": {}, + "source": [ + "# OpenWeatherMap API\n", + "\n", + "This notebook goes over how to use the OpenWeatherMap component to fetch weather information.\n", + "\n", + "First, you need to sign up for an OpenWeatherMap API key:\n", + "\n", + "1. Go to OpenWeatherMap and sign up for an API key [here](https://openweathermap.org/api/)\n", + "2. pip install pyowm\n", + "\n", + "Then we will need to set some environment variables:\n", + "1. Save your API KEY into OPENWEATHERMAP_API_KEY env variable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "961b3689", + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "outputs": [], + "source": [ + "pip install pyowm" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "34bb5968", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"OPENWEATHERMAP_API_KEY\"] = \"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "ac4910f8", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.utilities import OpenWeatherMapAPIWrapper" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "84b8f773", + "metadata": {}, + "outputs": [], + "source": [ + "weather = OpenWeatherMapAPIWrapper()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "9651f324-e74a-4f08-a28a-89db029f66f8", + "metadata": {}, + "outputs": [], + "source": [ + "weather_data = weather.run(\"London,GB\")" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "028f4cba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "In London,GB, the current weather is as follows:\n", + "Detailed status: overcast clouds\n", + "Wind speed: 4.63 m/s, direction: 150°\n", + "Humidity: 67%\n", + "Temperature: \n", + " - Current: 5.35°C\n", + " - High: 6.26°C\n", + " - Low: 3.49°C\n", + " - Feels like: 1.95°C\n", + "Rain: {}\n", + "Heat index: None\n", + "Cloud cover: 100%\n" + ] + } + ], + "source": [ + "print(weather_data)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/modules/agents/tools/getting_started.md b/docs/modules/agents/tools/getting_started.md index 8af18f3c..8e233536 100644 --- a/docs/modules/agents/tools/getting_started.md +++ b/docs/modules/agents/tools/getting_started.md @@ -152,3 +152,11 @@ Below is a list of all supported tools and relevant information: - Notes: A natural language connection to the Listen Notes Podcast API (`https://www.PodcastAPI.com`), specifically the `/search/` endpoint. - Requires LLM: Yes - Extra Parameters: `listen_api_key` (your api key to access this endpoint) + +**openweathermap-api** + +- Tool Name: OpenWeatherMap +- Tool Description: A wrapper around OpenWeatherMap API. Useful for fetching current weather information for a specified location. Input should be a location string (e.g. 'London,GB'). +- Notes: A connection to the OpenWeatherMap API (https://api.openweathermap.org), specifically the `/data/2.5/weather` endpoint. +- Requires LLM: No +- Extra Parameters: `openweathermap_api_key` (your API key to access this endpoint) diff --git a/langchain/tools/openweathermap/__init__.py b/langchain/tools/openweathermap/__init__.py new file mode 100644 index 00000000..9c9cff1a --- /dev/null +++ b/langchain/tools/openweathermap/__init__.py @@ -0,0 +1 @@ +"""OpenWeatherMap API toolkit.""" diff --git a/langchain/tools/openweathermap/tool.py b/langchain/tools/openweathermap/tool.py new file mode 100644 index 00000000..5c2cb34e --- /dev/null +++ b/langchain/tools/openweathermap/tool.py @@ -0,0 +1,29 @@ +"""Tool for the OpenWeatherMap API.""" + +from langchain.tools.base import BaseTool +from langchain.utilities import OpenWeatherMapAPIWrapper + + +class OpenWeatherMapQueryRun(BaseTool): + """Tool that adds the capability to query using the OpenWeatherMap API.""" + + api_wrapper: OpenWeatherMapAPIWrapper + + name = "OpenWeatherMap" + description = ( + "A wrapper around OpenWeatherMap API. " + "Useful for fetching current weather information for a specified location. " + "Input should be a location string (e.g. 'London,GB')." + ) + + def __init__(self) -> None: + self.api_wrapper = OpenWeatherMapAPIWrapper() + return + + def _run(self, location: str) -> str: + """Use the OpenWeatherMap tool.""" + return self.api_wrapper.run(location) + + async def _arun(self, location: str) -> str: + """Use the OpenWeatherMap tool asynchronously.""" + raise NotImplementedError("OpenWeatherMapQueryRun does not support async") diff --git a/langchain/utilities/__init__.py b/langchain/utilities/__init__.py index 058ab5f7..b8103348 100644 --- a/langchain/utilities/__init__.py +++ b/langchain/utilities/__init__.py @@ -5,6 +5,7 @@ from langchain.utilities.bash import BashProcess from langchain.utilities.bing_search import BingSearchAPIWrapper from langchain.utilities.google_search import GoogleSearchAPIWrapper from langchain.utilities.google_serper import GoogleSerperAPIWrapper +from langchain.utilities.openweathermap import OpenWeatherMapAPIWrapper from langchain.utilities.searx_search import SearxSearchWrapper from langchain.utilities.serpapi import SerpAPIWrapper from langchain.utilities.wikipedia import WikipediaAPIWrapper @@ -21,4 +22,5 @@ __all__ = [ "SearxSearchWrapper", "BingSearchAPIWrapper", "WikipediaAPIWrapper", + "OpenWeatherMapAPIWrapper", ] diff --git a/langchain/utilities/openweathermap.py b/langchain/utilities/openweathermap.py new file mode 100644 index 00000000..eac6109c --- /dev/null +++ b/langchain/utilities/openweathermap.py @@ -0,0 +1,79 @@ +"""Util that calls OpenWeatherMap using PyOWM.""" +from typing import Any, Dict, Optional + +from pydantic import Extra, root_validator + +from langchain.tools.base import BaseModel +from langchain.utils import get_from_dict_or_env + + +class OpenWeatherMapAPIWrapper(BaseModel): + """Wrapper for OpenWeatherMap API using PyOWM. + + Docs for using: + + 1. Go to OpenWeatherMap and sign up for an API key + 3. Save your API KEY into OPENWEATHERMAP_API_KEY env variable + 4. pip install wolframalpha + """ + + owm: Any + openweathermap_api_key: Optional[str] = None + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + + @root_validator(pre=True) + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key exists in environment.""" + openweathermap_api_key = get_from_dict_or_env( + values, "openweathermap_api_key", "OPENWEATHERMAP_API_KEY" + ) + values["openweathermap_api_key"] = openweathermap_api_key + + try: + import pyowm + + except ImportError: + raise ImportError( + "pyowm is not installed. " "Please install it with `pip install pyowm`" + ) + + owm = pyowm.OWM(openweathermap_api_key) + values["owm"] = owm + + return values + + def _format_weather_info(self, location: str, w: Any) -> str: + detailed_status = w.detailed_status + wind = w.wind() + humidity = w.humidity + temperature = w.temperature("celsius") + rain = w.rain + heat_index = w.heat_index + clouds = w.clouds + + return ( + f"In {location}, the current weather is as follows:\n" + f"Detailed status: {detailed_status}\n" + f"Wind speed: {wind['speed']} m/s, direction: {wind['deg']}°\n" + f"Humidity: {humidity}%\n" + f"Temperature: \n" + f" - Current: {temperature['temp']}°C\n" + f" - High: {temperature['temp_max']}°C\n" + f" - Low: {temperature['temp_min']}°C\n" + f" - Feels like: {temperature['feels_like']}°C\n" + f"Rain: {rain}\n" + f"Heat index: {heat_index}\n" + f"Cloud cover: {clouds}%" + ) + + def run(self, location: str) -> str: + """Get the current weather information for a specified location.""" + mgr = self.owm.weather_manager() + observation = mgr.weather_at_place(location) + w = observation.weather + + return self._format_weather_info(location, w) diff --git a/poetry.lock b/poetry.lock index 0c77bce5..eddd25e3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. [[package]] name = "absl-py" @@ -1779,6 +1779,18 @@ files = [ {file = "gast-0.4.0.tar.gz", hash = "sha256:40feb7b8b8434785585ab224d1568b857edb18297e5a3047f1ba012bc83b42c1"}, ] +[[package]] +name = "geojson" +version = "2.5.0" +description = "Python bindings and utilities for GeoJSON" +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "geojson-2.5.0-py2.py3-none-any.whl", hash = "sha256:ccbd13368dd728f4e4f13ffe6aaf725b6e802c692ba0dde628be475040c534ba"}, + {file = "geojson-2.5.0.tar.gz", hash = "sha256:6e4bb7ace4226a45d9c8c8b1348b3fc43540658359f93c3f7e03efa9f15f658a"}, +] + [[package]] name = "google-api-core" version = "2.11.0" @@ -5359,6 +5371,26 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +[[package]] +name = "pyowm" +version = "3.3.0" +description = "A Python wrapper around OpenWeatherMap web APIs" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyowm-3.3.0-py3-none-any.whl", hash = "sha256:86463108e7613171531ba306040b43c972b3fc0b0acf73b12c50910cdd2107ab"}, + {file = "pyowm-3.3.0.tar.gz", hash = "sha256:8196f77c91eac680676ed5ee484aae8a165408055e3e2b28025cbf60b8681e03"}, +] + +[package.dependencies] +geojson = ">=2.3.0,<3" +PySocks = ">=1.7.1,<2" +requests = [ + {version = ">=2.20.0,<3"}, + {version = "*", extras = ["socks"]}, +] + [[package]] name = "pyparsing" version = "3.0.9" @@ -5433,6 +5465,19 @@ files = [ {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"}, ] +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + [[package]] name = "pytest" version = "7.2.2" @@ -5968,6 +6013,7 @@ files = [ certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""} urllib3 = ">=1.21.1,<1.27" [package.extras] @@ -6822,7 +6868,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""} [package.extras] aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] @@ -8482,10 +8528,10 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] -all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "jina", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence-transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx", "nomic", "aleph-alpha-client", "deeplake", "pgvector", "psycopg2-binary"] -llms = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "torch", "transformers"] +all = ["aleph-alpha-client", "anthropic", "beautifulsoup4", "cohere", "deeplake", "elasticsearch", "faiss-cpu", "google-api-python-client", "google-search-results", "huggingface_hub", "jina", "jinja2", "manifest-ml", "networkx", "nlpcloud", "nltk", "nomic", "openai", "opensearch-py", "pgvector", "pinecone-client", "psycopg2-binary", "pypdf", "qdrant-client", "redis", "sentence-transformers", "spacy", "tensorflow-text", "tiktoken", "torch", "transformers", "weaviate-client", "wikipedia", "wolframalpha"] +llms = ["anthropic", "cohere", "huggingface_hub", "manifest-ml", "nlpcloud", "openai", "torch", "transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "4986fe2bbc54d9c31181a843f7b3e0a9b1b46ad833fc24e52f1c181fe2ba54f5" +content-hash = "fec488d52fc1a46ae34a643e5951e00251232d6d38c50c844556041df6067572" diff --git a/pyproject.toml b/pyproject.toml index 16cac6a6..8bb4d89a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ aleph-alpha-client = {version="^2.15.0", optional = true} deeplake = {version = "^3.2.9", optional = true} pgvector = {version = "^0.1.6", optional = true} psycopg2-binary = {version = "^2.9.5", optional = true} - +pyowm = {version = "^3.3.0", optional = true} [tool.poetry.group.docs.dependencies] autodoc_pydantic = "^1.8.0" diff --git a/tests/integration_tests/test_openweathermap.py b/tests/integration_tests/test_openweathermap.py new file mode 100644 index 00000000..8bbf476d --- /dev/null +++ b/tests/integration_tests/test_openweathermap.py @@ -0,0 +1,24 @@ +from langchain.utilities.openweathermap import OpenWeatherMapAPIWrapper + + +def test_openweathermap_api_wrapper() -> None: + """Test that OpenWeatherMapAPIWrapper returns correct data for London, GB.""" + + weather = OpenWeatherMapAPIWrapper() + weather_data = weather.run("London,GB") + + assert weather_data is not None + assert "London" in weather_data + assert "GB" in weather_data + assert "Detailed status:" in weather_data + assert "Wind speed:" in weather_data + assert "direction:" in weather_data + assert "Humidity:" in weather_data + assert "Temperature:" in weather_data + assert "Current:" in weather_data + assert "High:" in weather_data + assert "Low:" in weather_data + assert "Feels like:" in weather_data + assert "Rain:" in weather_data + assert "Heat index:" in weather_data + assert "Cloud cover:" in weather_data