Merriam-Webster Dictionary Tool (#12044)

# Description

We implemented a simple tool for accessing the Merriam-Webster
Collegiate Dictionary API
(https://dictionaryapi.com/products/api-collegiate-dictionary).

Here's a simple usage example:

```py
from langchain.llms import OpenAI
from langchain.agents import load_tools, initialize_agent, AgentType

llm = OpenAI()
tools = load_tools(["serpapi", "merriam-webster"], llm=llm) # Serp API gives our agent access to Google
agent = initialize_agent(
  tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
agent.run("What is the english word for the german word Himbeere? Define that word.")
```

Sample output:

```
> Entering new AgentExecutor chain...
 I need to find the english word for Himbeere and then get the definition of that word.
Action: Search
Action Input: "English word for Himbeere"
Observation: {'type': 'translation_result'}
Thought: Now I have the english word, I can look up the definition.
Action: MerriamWebster
Action Input: raspberry
Observation: Definitions of 'raspberry':

1. rasp-ber-ry, noun: any of various usually black or red edible berries that are aggregate fruits consisting of numerous small drupes on a fleshy receptacle and that are usually rounder and smaller than the closely related blackberries
2. rasp-ber-ry, noun: a perennial plant (genus Rubus) of the rose family that bears raspberries
3. rasp-ber-ry, noun: a sound of contempt made by protruding the tongue between the lips and expelling air forcibly to produce a vibration; broadly : an expression of disapproval or contempt
4. black raspberry, noun: a raspberry (Rubus occidentalis) of eastern North America that has a purplish-black fruit and is the source of several cultivated varieties —called also blackcap

Thought: I now know the final answer.
Final Answer: Raspberry is an english word for Himbeere and it is defined as any of various usually black or red edible berries that are aggregate fruits consisting of numerous small drupes on a fleshy receptacle and that are usually rounder and smaller than the closely related blackberries.

> Finished chain.
```

# Issue

This closes #12039.

# Dependencies

We added no extra dependencies.

<!-- Thank you for contributing to LangChain!

Replace this entire comment with:
  - **Description:** a description of the change, 
  - **Issue:** the issue # it fixes (if applicable),
  - **Dependencies:** any dependencies required for this change,
- **Tag maintainer:** for a quicker response, tag the relevant
maintainer (see below),
- **Twitter handle:** we announce bigger features on Twitter. If your PR
gets announced, and you'd like a mention, we'll gladly shout you out!

Please make sure your PR is passing linting and testing before
submitting. Run `make format`, `make lint` and `make test` to check this
locally.

See contribution guidelines for more information on how to write/run
tests, lint, etc:

https://github.com/langchain-ai/langchain/blob/master/.github/CONTRIBUTING.md

If you're adding a new integration, please include:
1. a test for the integration, preferably unit tests that do not rely on
network access,
2. an example notebook showing its use. It lives in `docs/extras`
directory.

If no one reviews your PR within a few days, please @-mention one of
@baskaryan, @eyurtsev, @hwchase17.
 -->

---------

Co-authored-by: Lara <63805048+larkgz@users.noreply.github.com>
Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>
This commit is contained in:
Josef Zoller 2023-11-29 20:28:29 -05:00 committed by GitHub
parent f3dd4a10cf
commit c2e3963da4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 196 additions and 0 deletions

View File

@ -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,

View File

@ -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",

View File

@ -0,0 +1 @@
"""Merriam-Webster API toolkit."""

View File

@ -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)

View File

@ -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",

View File

@ -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}"

View File

@ -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

View File

@ -112,6 +112,7 @@ EXPECTED_ALL = [
"authenticate",
"format_tool_to_openai_function",
"tool",
"MerriamWebsterQueryRun",
]

View File

@ -67,6 +67,7 @@ _EXPECTED = [
"ListPowerBITool",
"ListSQLDatabaseTool",
"ListSparkSQLTool",
"MerriamWebsterQueryRun",
"MetaphorSearchResults",
"MoveFileTool",
"NavigateBackTool",

View File

@ -44,6 +44,7 @@ EXPECTED_ALL = [
"WikipediaAPIWrapper",
"WolframAlphaAPIWrapper",
"ZapierNLAWrapper",
"MerriamWebsterAPIWrapper",
]