diff --git a/libs/langchain/langchain/agents/load_tools.py b/libs/langchain/langchain/agents/load_tools.py index 7aadda4e812..28e95c62265 100644 --- a/libs/langchain/langchain/agents/load_tools.py +++ b/libs/langchain/langchain/agents/load_tools.py @@ -58,6 +58,7 @@ from langchain.tools.searx_search.tool import SearxSearchResults, SearxSearchRun from langchain.tools.shell.tool import ShellTool from langchain.tools.sleep.tool import SleepTool from langchain.tools.stackexchange.tool import StackExchangeTool +from langchain.tools.merriam_webster.tool import MerriamWebsterQueryRun from langchain.tools.wikipedia.tool import WikipediaQueryRun from langchain.tools.wolfram_alpha.tool import WolframAlphaQueryRun from langchain.tools.openweathermap.tool import OpenWeatherMapQueryRun @@ -85,6 +86,7 @@ from langchain.utilities.searx_search import SearxSearchWrapper from langchain.utilities.serpapi import SerpAPIWrapper from langchain.utilities.stackexchange import StackExchangeAPIWrapper from langchain.utilities.twilio import TwilioAPIWrapper +from langchain.utilities.merriam_webster import MerriamWebsterAPIWrapper from langchain.utilities.wikipedia import WikipediaAPIWrapper from langchain.utilities.wolfram_alpha import WolframAlphaAPIWrapper from langchain.utilities.openweathermap import OpenWeatherMapAPIWrapper @@ -232,6 +234,10 @@ def _get_google_search(**kwargs: Any) -> BaseTool: return GoogleSearchRun(api_wrapper=GoogleSearchAPIWrapper(**kwargs)) +def _get_merriam_webster(**kwargs: Any) -> BaseTool: + return MerriamWebsterQueryRun(api_wrapper=MerriamWebsterAPIWrapper(**kwargs)) + + def _get_wikipedia(**kwargs: Any) -> BaseTool: return WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(**kwargs)) @@ -434,6 +440,7 @@ _EXTRA_OPTIONAL_TOOLS: Dict[str, Tuple[Callable[[KwArg(Any)], BaseTool], List[st "dalle-image-generator": (_get_dalle_image_generator, ["openai_api_key"]), "twilio": (_get_twilio, ["account_sid", "auth_token", "from_number"]), "searx-search": (_get_searx_search, ["searx_host", "engines", "aiosession"]), + "merriam-webster": (_get_merriam_webster, ["merriam_webster_api_key"]), "wikipedia": (_get_wikipedia, ["top_k_results", "lang"]), "arxiv": ( _get_arxiv, diff --git a/libs/langchain/langchain/tools/__init__.py b/libs/langchain/langchain/tools/__init__.py index 15d5c922ff7..db0e271c6c2 100644 --- a/libs/langchain/langchain/tools/__init__.py +++ b/libs/langchain/langchain/tools/__init__.py @@ -326,6 +326,12 @@ def _import_json_tool_JsonListKeysTool() -> Any: return JsonListKeysTool +def _import_merriam_webster_tool() -> Any: + from langchain.tools.merriam_webster.tool import MerriamWebsterQueryRun + + return MerriamWebsterQueryRun + + def _import_metaphor_search() -> Any: from langchain.tools.metaphor_search import MetaphorSearchResults @@ -791,6 +797,8 @@ def __getattr__(name: str) -> Any: return _import_json_tool_JsonGetValueTool() elif name == "JsonListKeysTool": return _import_json_tool_JsonListKeysTool() + elif name == "MerriamWebsterQueryRun": + return _import_merriam_webster_tool() elif name == "MetaphorSearchResults": return _import_metaphor_search() elif name == "O365CreateDraftMessage": @@ -979,6 +987,7 @@ __all__ = [ "ListPowerBITool", "ListSQLDatabaseTool", "ListSparkSQLTool", + "MerriamWebsterQueryRun", "MetaphorSearchResults", "MoveFileTool", "NavigateBackTool", diff --git a/libs/langchain/langchain/tools/merriam_webster/__init__.py b/libs/langchain/langchain/tools/merriam_webster/__init__.py new file mode 100644 index 00000000000..73390d54980 --- /dev/null +++ b/libs/langchain/langchain/tools/merriam_webster/__init__.py @@ -0,0 +1 @@ +"""Merriam-Webster API toolkit.""" diff --git a/libs/langchain/langchain/tools/merriam_webster/tool.py b/libs/langchain/langchain/tools/merriam_webster/tool.py new file mode 100644 index 00000000000..a92f970468e --- /dev/null +++ b/libs/langchain/langchain/tools/merriam_webster/tool.py @@ -0,0 +1,27 @@ +"""Tool for the Merriam-Webster API.""" + +from typing import Optional + +from langchain.callbacks.manager import CallbackManagerForToolRun +from langchain.tools.base import BaseTool +from langchain.utilities.merriam_webster import MerriamWebsterAPIWrapper + + +class MerriamWebsterQueryRun(BaseTool): + """Tool that searches the Merriam-Webster API.""" + + name: str = "MerriamWebster" + description: str = ( + "A wrapper around Merriam-Webster. " + "Useful for when you need to get the definition of a word." + "Input should be the word you want the definition of." + ) + api_wrapper: MerriamWebsterAPIWrapper + + def _run( + self, + query: str, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + """Use the Merriam-Webster tool.""" + return self.api_wrapper.run(query) diff --git a/libs/langchain/langchain/utilities/__init__.py b/libs/langchain/langchain/utilities/__init__.py index 3014fcb088a..817a5475ab3 100644 --- a/libs/langchain/langchain/utilities/__init__.py +++ b/libs/langchain/langchain/utilities/__init__.py @@ -134,6 +134,12 @@ def _import_max_compute() -> Any: return MaxComputeAPIWrapper +def _import_merriam_webster() -> Any: + from langchain.utilities.merriam_webster import MerriamWebsterAPIWrapper + + return MerriamWebsterAPIWrapper + + def _import_metaphor_search() -> Any: from langchain.utilities.metaphor_search import MetaphorSearchAPIWrapper @@ -291,6 +297,8 @@ def __getattr__(name: str) -> Any: return _import_jira() elif name == "MaxComputeAPIWrapper": return _import_max_compute() + elif name == "MerriamWebsterAPIWrapper": + return _import_merriam_webster() elif name == "MetaphorSearchAPIWrapper": return _import_metaphor_search() elif name == "OpenWeatherMapAPIWrapper": @@ -355,6 +363,7 @@ __all__ = [ "JiraAPIWrapper", "LambdaWrapper", "MaxComputeAPIWrapper", + "MerriamWebsterAPIWrapper", "MetaphorSearchAPIWrapper", "OpenWeatherMapAPIWrapper", "OutlineAPIWrapper", diff --git a/libs/langchain/langchain/utilities/merriam_webster.py b/libs/langchain/langchain/utilities/merriam_webster.py new file mode 100644 index 00000000000..432d672415c --- /dev/null +++ b/libs/langchain/langchain/utilities/merriam_webster.py @@ -0,0 +1,108 @@ +"""Util that calls Merriam-Webster.""" +import json +from typing import Dict, Iterator, List, Optional +from urllib.parse import quote + +import requests + +from langchain.pydantic_v1 import BaseModel, Extra, root_validator +from langchain.utils import get_from_dict_or_env + +MERRIAM_WEBSTER_API_URL = ( + "https://www.dictionaryapi.com/api/v3/references/collegiate/json" +) +MERRIAM_WEBSTER_TIMEOUT = 5000 + + +class MerriamWebsterAPIWrapper(BaseModel): + """Wrapper for Merriam-Webster. + + Docs for using: + + 1. Go to https://www.dictionaryapi.com/register/index and register an + developer account with a key for the Collegiate Dictionary + 2. Get your API Key from https://www.dictionaryapi.com/account/my-keys + 3. Save your API Key into MERRIAM_WEBSTER_API_KEY env variable + + """ + + merriam_webster_api_key: Optional[str] = None + + class Config: + """Configuration for this pydantic object.""" + + extra = Extra.forbid + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key exists in environment.""" + merriam_webster_api_key = get_from_dict_or_env( + values, "merriam_webster_api_key", "MERRIAM_WEBSTER_API_KEY" + ) + values["merriam_webster_api_key"] = merriam_webster_api_key + + return values + + def run(self, query: str) -> str: + """Run query through Merriam-Webster API and return a formatted result.""" + quoted_query = quote(query) + + request_url = ( + f"{MERRIAM_WEBSTER_API_URL}/{quoted_query}" + f"?key={self.merriam_webster_api_key}" + ) + + response = requests.get(request_url, timeout=MERRIAM_WEBSTER_TIMEOUT) + + if response.status_code != 200: + return response.text + + return self._format_response(query, response) + + def _format_response(self, query: str, response: requests.Response) -> str: + content = json.loads(response.content) + + if not content: + return f"No Merriam-Webster definition was found for query '{query}'." + + if isinstance(content[0], str): + result = f"No Merriam-Webster definition was found for query '{query}'.\n" + if len(content) > 1: + alternatives = [f"{i + 1}. {content[i]}" for i in range(len(content))] + result += "You can try one of the following alternative queries:\n\n" + result += "\n".join(alternatives) + else: + result += f"Did you mean '{content[0]}'?" + else: + result = self._format_definitions(query, content) + + return result + + def _format_definitions(self, query: str, definitions: List[Dict]) -> str: + formatted_definitions: List[str] = [] + for definition in definitions: + formatted_definitions.extend(self._format_definition(definition)) + + if len(formatted_definitions) == 1: + return f"Definition of '{query}':\n" f"{formatted_definitions[0]}" + + result = f"Definitions of '{query}':\n\n" + for i, formatted_definition in enumerate(formatted_definitions, 1): + result += f"{i}. {formatted_definition}\n" + + return result + + def _format_definition(self, definition: Dict) -> Iterator[str]: + if "hwi" in definition: + headword = definition["hwi"]["hw"].replace("*", "-") + else: + headword = definition["meta"]["id"].split(":")[0] + + if "fl" in definition: + functional_label = definition["fl"] + + if "shortdef" in definition: + for short_def in definition["shortdef"]: + yield f"{headword}, {functional_label}: {short_def}" + else: + yield f"{headword}, {functional_label}" diff --git a/libs/langchain/tests/integration_tests/utilities/test_merriam_webster_api.py b/libs/langchain/tests/integration_tests/utilities/test_merriam_webster_api.py new file mode 100644 index 00000000000..4d0b996a3fa --- /dev/null +++ b/libs/langchain/tests/integration_tests/utilities/test_merriam_webster_api.py @@ -0,0 +1,32 @@ +"""Integration test for Merriam Webster API Wrapper.""" +import pytest + +from langchain.utilities.merriam_webster import MerriamWebsterAPIWrapper + + +@pytest.fixture +def api_client() -> MerriamWebsterAPIWrapper: + return MerriamWebsterAPIWrapper() + + +def test_call(api_client: MerriamWebsterAPIWrapper) -> None: + """Test that call gives correct answer.""" + output = api_client.run("LLM") + assert "large language model" in output + + +def test_call_no_result(api_client: MerriamWebsterAPIWrapper) -> None: + """Test that non-existent words return proper result.""" + output = api_client.run("NO_RESULT_NO_RESULT_NO_RESULT") + assert "No Merriam-Webster definition was found for query" in output + + +def test_call_alternatives(api_client: MerriamWebsterAPIWrapper) -> None: + """ + Test that non-existent queries that are close to an + existing definition return proper result. + """ + output = api_client.run("It's raining cats and dogs") + assert "No Merriam-Webster definition was found for query" in output + assert "You can try one of the following alternative queries" in output + assert "raining cats and dogs" in output diff --git a/libs/langchain/tests/unit_tests/tools/test_imports.py b/libs/langchain/tests/unit_tests/tools/test_imports.py index 8307c347db9..0f9869ce103 100644 --- a/libs/langchain/tests/unit_tests/tools/test_imports.py +++ b/libs/langchain/tests/unit_tests/tools/test_imports.py @@ -112,6 +112,7 @@ EXPECTED_ALL = [ "authenticate", "format_tool_to_openai_function", "tool", + "MerriamWebsterQueryRun", ] diff --git a/libs/langchain/tests/unit_tests/tools/test_public_api.py b/libs/langchain/tests/unit_tests/tools/test_public_api.py index fc55448935c..4bdc848033a 100644 --- a/libs/langchain/tests/unit_tests/tools/test_public_api.py +++ b/libs/langchain/tests/unit_tests/tools/test_public_api.py @@ -67,6 +67,7 @@ _EXPECTED = [ "ListPowerBITool", "ListSQLDatabaseTool", "ListSparkSQLTool", + "MerriamWebsterQueryRun", "MetaphorSearchResults", "MoveFileTool", "NavigateBackTool", diff --git a/libs/langchain/tests/unit_tests/utilities/test_imports.py b/libs/langchain/tests/unit_tests/utilities/test_imports.py index 3d564184b8f..f1e6a27eda9 100644 --- a/libs/langchain/tests/unit_tests/utilities/test_imports.py +++ b/libs/langchain/tests/unit_tests/utilities/test_imports.py @@ -44,6 +44,7 @@ EXPECTED_ALL = [ "WikipediaAPIWrapper", "WolframAlphaAPIWrapper", "ZapierNLAWrapper", + "MerriamWebsterAPIWrapper", ]