From e46cd3b7dbe931b7a1d384e1d3115c9fc62bfadb Mon Sep 17 00:00:00 2001 From: rogerserper <124558887+rogerserper@users.noreply.github.com> Date: Thu, 16 Feb 2023 07:47:17 +0100 Subject: [PATCH] =?UTF-8?q?Google=20Search=20API=20integration=20with=20se?= =?UTF-8?q?rper.dev=20(wrapper,=20tests,=20docs,=20=E2=80=A6=20(#909)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Google Search integration with [Serper](https://serper.dev) a low-cost alternative to SerpAPI (10x cheaper + generous free tier). Includes documentation, tests and examples. Hopefully I am not missing anything. Developers can sign up for a free account at [serper.dev](https://serper.dev) and obtain an api key. ## Usage ```python from langchain.utilities import GoogleSerperAPIWrapper from langchain.llms.openai import OpenAI from langchain.agents import initialize_agent, Tool import os os.environ["SERPER_API_KEY"] = "" os.environ['OPENAI_API_KEY'] = "" llm = OpenAI(temperature=0) search = GoogleSerperAPIWrapper() tools = [ Tool( name="Intermediate Answer", func=search.run ) ] self_ask_with_search = initialize_agent(tools, llm, agent="self-ask-with-search", verbose=True) self_ask_with_search.run("What is the hometown of the reigning men's U.S. Open champion?") ``` ### Output ``` Entering new AgentExecutor chain... Yes. Follow up: Who is the reigning men's U.S. Open champion? Intermediate answer: Current champions Carlos Alcaraz, 2022 men's singles champion. Follow up: Where is Carlos Alcaraz from? Intermediate answer: El Palmar, Spain So the final answer is: El Palmar, Spain > Finished chain. 'El Palmar, Spain' ``` --- docs/ecosystem/google_serper.md | 70 ++++++++ .../agents/examples/search_tools.ipynb | 165 +++++++++++++----- docs/modules/agents/tools.md | 9 + .../utils/examples/google_serper.ipynb | 157 +++++++++++++++++ langchain/__init__.py | 2 + langchain/agents/load_tools.py | 10 ++ langchain/agents/self_ask_with_search/base.py | 14 +- langchain/utilities/__init__.py | 2 + langchain/utilities/google_serper.py | 94 ++++++++++ .../chains/test_self_ask_with_search.py | 4 +- .../test_googleserper_api.py | 9 + 11 files changed, 489 insertions(+), 47 deletions(-) create mode 100644 docs/ecosystem/google_serper.md create mode 100644 docs/modules/utils/examples/google_serper.ipynb create mode 100644 langchain/utilities/google_serper.py create mode 100644 tests/integration_tests/test_googleserper_api.py diff --git a/docs/ecosystem/google_serper.md b/docs/ecosystem/google_serper.md new file mode 100644 index 00000000..f9f68f4b --- /dev/null +++ b/docs/ecosystem/google_serper.md @@ -0,0 +1,70 @@ +# Google Serper Wrapper + +This page covers how to use the [Serper](https://serper.dev) Google Search API within LangChain. Serper is a low-cost Google Search API that can be used to add answer box, knowledge graph, and organic results data from Google Search. +It is broken into two parts: setup, and then references to the specific Google Serper wrapper. + +## Setup +- Go to [serper.dev](https://serper.dev) to sign up for a free account +- Get the api key and set it as an environment variable (`SERPER_API_KEY`) + +## Wrappers + +### Utility + +There exists a GoogleSerperAPIWrapper utility which wraps this API. To import this utility: + +```python +from langchain.utilities import GoogleSerperAPIWrapper +``` + +You can use it as part of a Self Ask chain: + +```python +from langchain.utilities import GoogleSerperAPIWrapper +from langchain.llms.openai import OpenAI +from langchain.agents import initialize_agent, Tool + +import os +os.environ["SERPER_API_KEY"] = "" +os.environ['OPENAI_API_KEY'] = "" + +llm = OpenAI(temperature=0) +search = GoogleSerperAPIWrapper() +tools = [ + Tool( + name="Intermediate Answer", + func=search.run + ) +] + +self_ask_with_search = initialize_agent(tools, llm, agent="self-ask-with-search", verbose=True) +self_ask_with_search.run("What is the hometown of the reigning men's U.S. Open champion?") +``` + +#### Output +``` +Entering new AgentExecutor chain... + Yes. +Follow up: Who is the reigning men's U.S. Open champion? +Intermediate answer: Current champions Carlos Alcaraz, 2022 men's singles champion. +Follow up: Where is Carlos Alcaraz from? +Intermediate answer: El Palmar, Spain +So the final answer is: El Palmar, Spain + +> Finished chain. + +'El Palmar, Spain' +``` + +For a more detailed walkthrough of this wrapper, see [this notebook](../modules/utils/examples/google_serper.ipynb). + +### Tool + +You can also easily load this wrapper as a Tool (to use with an Agent). +You can do this with: +```python +from langchain.agents import load_tools +tools = load_tools(["google-serper"]) +``` + +For more information on this, see [this page](../modules/agents/tools.md) diff --git a/docs/modules/agents/examples/search_tools.ipynb b/docs/modules/agents/examples/search_tools.ipynb index 5cdba823..1320f79c 100644 --- a/docs/modules/agents/examples/search_tools.ipynb +++ b/docs/modules/agents/examples/search_tools.ipynb @@ -12,9 +12,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "e6860c2d", - "metadata": {}, + "metadata": { + "pycharm": { + "is_executing": true + } + }, "outputs": [], "source": [ "from langchain.agents import load_tools\n", @@ -34,39 +38,111 @@ }, { "cell_type": "markdown", - "id": "a09ca013", - "metadata": {}, + "source": [ + "## Google Serper API Wrapper\n", + "\n", + "First, let's try to use the Google Serper API tool." + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 4, + "outputs": [], + "source": [ + "tools = load_tools([\"google-serper\"], llm=llm)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 5, + "outputs": [], + "source": [ + "agent = initialize_agent(tools, llm, agent=\"zero-shot-react-description\", verbose=True)" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 6, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", + "\u001B[32;1m\u001B[1;3m I need to find out what the current weather is in Pomfret.\n", + "Action: Search\n", + "Action Input: \"weather in Pomfret\"\u001B[0m\n", + "Observation: \u001B[36;1m\u001B[1;3mShowers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001B[0m\n", + "Thought:\u001B[32;1m\u001B[1;3m I now know the current weather in Pomfret.\n", + "Final Answer: Showers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001B[0m\n", + "\u001B[1m> Finished AgentExecutor chain.\u001B[0m\n" + ] + }, + { + "data": { + "text/plain": [ + "'Showers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.'" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "agent.run(\"What is the weather in Pomfret?\")" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "markdown", "source": [ "## SerpAPI\n", "\n", - "First, let's use the SerpAPI tool." - ] + "Now, let's use the SerpAPI tool." + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": 4, - "id": "dd4ce6d9", - "metadata": {}, "outputs": [], "source": [ "tools = load_tools([\"serpapi\"], llm=llm)" - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": 5, - "id": "ef63bb84", - "metadata": {}, "outputs": [], "source": [ "agent = initialize_agent(tools, llm, agent=\"zero-shot-react-description\", verbose=True)" - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": 6, - "id": "53e24f5d", - "metadata": {}, "outputs": [ { "name": "stdout", @@ -74,14 +150,14 @@ "text": [ "\n", "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m I need to find out what the current weather is in Pomfret.\n", + "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", + "\u001B[32;1m\u001B[1;3m I need to find out what the current weather is in Pomfret.\n", "Action: Search\n", - "Action Input: \"weather in Pomfret\"\u001b[0m\n", - "Observation: \u001b[36;1m\u001b[1;3mShowers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I now know the current weather in Pomfret.\n", - "Final Answer: Showers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001b[0m\n", - "\u001b[1m> Finished AgentExecutor chain.\u001b[0m\n" + "Action Input: \"weather in Pomfret\"\u001B[0m\n", + "Observation: \u001B[36;1m\u001B[1;3mShowers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001B[0m\n", + "Thought:\u001B[32;1m\u001B[1;3m I now know the current weather in Pomfret.\n", + "Final Answer: Showers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001B[0m\n", + "\u001B[1m> Finished AgentExecutor chain.\u001B[0m\n" ] }, { @@ -97,43 +173,47 @@ ], "source": [ "agent.run(\"What is the weather in Pomfret?\")" - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "markdown", - "id": "8ef49137", - "metadata": {}, "source": [ "## GoogleSearchAPIWrapper\n", "\n", "Now, let's use the official Google Search API Wrapper." - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": 13, - "id": "3e9c7c20", - "metadata": {}, "outputs": [], "source": [ "tools = load_tools([\"google-search\"], llm=llm)" - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": 14, - "id": "b83624dc", - "metadata": {}, "outputs": [], "source": [ "agent = initialize_agent(tools, llm, agent=\"zero-shot-react-description\", verbose=True)" - ] + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", "execution_count": 17, - "id": "9d5835e2", - "metadata": {}, "outputs": [ { "name": "stdout", @@ -141,14 +221,14 @@ "text": [ "\n", "\n", - "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n", - "\u001b[32;1m\u001b[1;3m I should look up the current weather conditions.\n", + "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", + "\u001B[32;1m\u001B[1;3m I should look up the current weather conditions.\n", "Action: Google Search\n", - "Action Input: \"weather in Pomfret\"\u001b[0m\n", - "Observation: \u001b[36;1m\u001b[1;3mShowers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%. Pomfret, CT Weather Forecast, with current conditions, wind, air quality, and what to expect for the next 3 days. Hourly Weather-Pomfret, CT. As of 12:52 am EST. Special Weather Statement +2 ... Hazardous Weather Conditions. Special Weather Statement ... Pomfret CT. Tonight ... National Digital Forecast Database Maximum Temperature Forecast. Pomfret Center Weather Forecasts. Weather Underground provides local & long-range weather forecasts, weatherreports, maps & tropical weather conditions for ... Pomfret, CT 12 hour by hour weather forecast includes precipitation, temperatures, sky conditions, rain chance, dew-point, relative humidity, wind direction ... North Pomfret Weather Forecasts. Weather Underground provides local & long-range weather forecasts, weatherreports, maps & tropical weather conditions for ... Today's Weather - Pomfret, CT. Dec 31, 2022 4:00 PM. Putnam MS. --. Weather forecast icon. Feels like --. Hi --. Lo --. Pomfret, CT temperature trend for the next 14 Days. Find daytime highs and nighttime lows from TheWeatherNetwork.com. Pomfret, MD Weather Forecast Date: 332 PM EST Wed Dec 28 2022. The area/counties/county of: Charles, including the cites of: St. Charles and Waldorf.\u001b[0m\n", - "Thought:\u001b[32;1m\u001b[1;3m I now know the current weather conditions in Pomfret.\n", - "Final Answer: Showers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001b[0m\n", - "\u001b[1m> Finished AgentExecutor chain.\u001b[0m\n" + "Action Input: \"weather in Pomfret\"\u001B[0m\n", + "Observation: \u001B[36;1m\u001B[1;3mShowers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%. Pomfret, CT Weather Forecast, with current conditions, wind, air quality, and what to expect for the next 3 days. Hourly Weather-Pomfret, CT. As of 12:52 am EST. Special Weather Statement +2 ... Hazardous Weather Conditions. Special Weather Statement ... Pomfret CT. Tonight ... National Digital Forecast Database Maximum Temperature Forecast. Pomfret Center Weather Forecasts. Weather Underground provides local & long-range weather forecasts, weatherreports, maps & tropical weather conditions for ... Pomfret, CT 12 hour by hour weather forecast includes precipitation, temperatures, sky conditions, rain chance, dew-point, relative humidity, wind direction ... North Pomfret Weather Forecasts. Weather Underground provides local & long-range weather forecasts, weatherreports, maps & tropical weather conditions for ... Today's Weather - Pomfret, CT. Dec 31, 2022 4:00 PM. Putnam MS. --. Weather forecast icon. Feels like --. Hi --. Lo --. Pomfret, CT temperature trend for the next 14 Days. Find daytime highs and nighttime lows from TheWeatherNetwork.com. Pomfret, MD Weather Forecast Date: 332 PM EST Wed Dec 28 2022. The area/counties/county of: Charles, including the cites of: St. Charles and Waldorf.\u001B[0m\n", + "Thought:\u001B[32;1m\u001B[1;3m I now know the current weather conditions in Pomfret.\n", + "Final Answer: Showers early becoming a steady light rain later in the day. Near record high temperatures. High around 60F. Winds SW at 10 to 15 mph. Chance of rain 60%.\u001B[0m\n", + "\u001B[1m> Finished AgentExecutor chain.\u001B[0m\n" ] }, { @@ -164,7 +244,10 @@ ], "source": [ "agent.run(\"What is the weather in Pomfret?\")" - ] + ], + "metadata": { + "collapsed": false + } } ], "metadata": { diff --git a/docs/modules/agents/tools.md b/docs/modules/agents/tools.md index 0aedd961..3a9dc719 100644 --- a/docs/modules/agents/tools.md +++ b/docs/modules/agents/tools.md @@ -119,3 +119,12 @@ Below is a list of all supported tools and relevant information: - Requires LLM: No - Extra Parameters: `google_api_key`, `google_cse_id` - For more information on this, see [this page](../../ecosystem/google_search.md) + +**google-serper** + +- Tool Name: Search +- Tool Description: A low-cost Google Search API. Useful for when you need to answer questions about current events. Input should be a search query. +- Notes: Calls the [serper.dev](https://serper.dev) Google Search API and then parses results. +- Requires LLM: No +- Extra Parameters: `serper_api_key` +- For more information on this, see [this page](../../ecosystem/google_serper.md) diff --git a/docs/modules/utils/examples/google_serper.ipynb b/docs/modules/utils/examples/google_serper.ipynb new file mode 100644 index 00000000..19354126 --- /dev/null +++ b/docs/modules/utils/examples/google_serper.ipynb @@ -0,0 +1,157 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dc23c48e", + "metadata": {}, + "source": [ + "# Google Serper API\n", + "\n", + "This notebook goes over how to use the Google Serper component to search the web. First you need to sign up for a free account at [serper.dev](https://serper.dev) and get your api key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"SERPER_API_KEY\"] = \"\"" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "54bf5afd", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.utilities import GoogleSerperAPIWrapper" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "31f8f382", + "metadata": {}, + "outputs": [], + "source": [ + "search = GoogleSerperAPIWrapper()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "25ce0225", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": "'Barack Hussein Obama II'" + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "search.run(\"Obama's first name?\")" + ] + }, + { + "cell_type": "markdown", + "source": [ + "## As part of a Self Ask With Search Chain" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "os.environ['OPENAI_API_KEY'] = \"\"" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": 5, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\u001B[1m> Entering new AgentExecutor chain...\u001B[0m\n", + "\u001B[32;1m\u001B[1;3m Yes.\n", + "Follow up: Who is the reigning men's U.S. Open champion?\u001B[0m\n", + "Intermediate answer: \u001B[36;1m\u001B[1;3mCurrent champions Carlos Alcaraz, 2022 men's singles champion.\u001B[0m\n", + "\u001B[32;1m\u001B[1;3mFollow up: Where is Carlos Alcaraz from?\u001B[0m\n", + "Intermediate answer: \u001B[36;1m\u001B[1;3mEl Palmar, Spain\u001B[0m\n", + "\u001B[32;1m\u001B[1;3mSo the final answer is: El Palmar, Spain\u001B[0m\n", + "\n", + "\u001B[1m> Finished chain.\u001B[0m\n" + ] + }, + { + "data": { + "text/plain": "'El Palmar, Spain'" + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from langchain.utilities import GoogleSerperAPIWrapper\n", + "from langchain.llms.openai import OpenAI\n", + "from langchain.agents import initialize_agent, Tool\n", + "\n", + "llm = OpenAI(temperature=0)\n", + "search = GoogleSerperAPIWrapper()\n", + "tools = [\n", + " Tool(\n", + " name=\"Intermediate Answer\",\n", + " func=search.run\n", + " )\n", + "]\n", + "\n", + "self_ask_with_search = initialize_agent(tools, llm, agent=\"self-ask-with-search\", verbose=True)\n", + "self_ask_with_search.run(\"What is the hometown of the reigning men's U.S. Open champion?\")" + ], + "metadata": { + "collapsed": false + } + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.10.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/langchain/__init__.py b/langchain/__init__.py index 4e2abef0..355e6245 100644 --- a/langchain/__init__.py +++ b/langchain/__init__.py @@ -42,6 +42,7 @@ from langchain.prompts import ( from langchain.serpapi import SerpAPIChain, SerpAPIWrapper from langchain.sql_database import SQLDatabase from langchain.utilities.google_search import GoogleSearchAPIWrapper +from langchain.utilities.google_serper import GoogleSerperAPIWrapper from langchain.utilities.wolfram_alpha import WolframAlphaAPIWrapper from langchain.vectorstores import FAISS, ElasticVectorSearch @@ -58,6 +59,7 @@ __all__ = [ "SerpAPIWrapper", "SerpAPIChain", "GoogleSearchAPIWrapper", + "GoogleSerperAPIWrapper", "WolframAlphaAPIWrapper", "Anthropic", "CerebriumAI", diff --git a/langchain/agents/load_tools.py b/langchain/agents/load_tools.py index 2c520973..65eac4d0 100644 --- a/langchain/agents/load_tools.py +++ b/langchain/agents/load_tools.py @@ -13,6 +13,7 @@ from langchain.requests import RequestsWrapper from langchain.serpapi import SerpAPIWrapper from langchain.utilities.bash import BashProcess from langchain.utilities.google_search import GoogleSearchAPIWrapper +from langchain.utilities.google_serper import GoogleSerperAPIWrapper from langchain.utilities.wolfram_alpha import WolframAlphaAPIWrapper @@ -131,6 +132,14 @@ def _get_google_search(**kwargs: Any) -> Tool: ) +def _get_google_serper(**kwargs: Any) -> Tool: + return Tool( + "Search", + GoogleSerperAPIWrapper(**kwargs).run, + "A low-cost Google Search API. Useful for when you need to answer questions about current events. Input should be a search query.", + ) + + def _get_serpapi(**kwargs: Any) -> Tool: return Tool( name="Search", @@ -147,6 +156,7 @@ _EXTRA_LLM_TOOLS = { _EXTRA_OPTIONAL_TOOLS = { "wolfram-alpha": (_get_wolfram_alpha, ["wolfram_alpha_appid"]), "google-search": (_get_google_search, ["google_api_key", "google_cse_id"]), + "google-serper": (_get_google_serper, ["serper_api_key"]), "serpapi": (_get_serpapi, ["serpapi_api_key", "aiosession"]), } diff --git a/langchain/agents/self_ask_with_search/base.py b/langchain/agents/self_ask_with_search/base.py index e542d125..aa3a9f44 100644 --- a/langchain/agents/self_ask_with_search/base.py +++ b/langchain/agents/self_ask_with_search/base.py @@ -1,5 +1,5 @@ """Chain that does self ask with search.""" -from typing import Any, List, Optional, Tuple +from typing import Any, List, Optional, Tuple, Union from langchain.agents.agent import Agent, AgentExecutor from langchain.agents.self_ask_with_search.prompt import PROMPT @@ -7,6 +7,7 @@ from langchain.agents.tools import Tool from langchain.llms.base import BaseLLM from langchain.prompts.base import BasePromptTemplate from langchain.serpapi import SerpAPIWrapper +from langchain.utilities.google_serper import GoogleSerperAPIWrapper class SelfAskWithSearchAgent(Agent): @@ -74,12 +75,17 @@ class SelfAskWithSearchChain(AgentExecutor): Example: .. code-block:: python - from langchain import SelfAskWithSearchChain, OpenAI, SerpAPIWrapper - search_chain = SerpAPIWrapper() + from langchain import SelfAskWithSearchChain, OpenAI, GoogleSerperAPIWrapper + search_chain = GoogleSerperAPIWrapper() self_ask = SelfAskWithSearchChain(llm=OpenAI(), search_chain=search_chain) """ - def __init__(self, llm: BaseLLM, search_chain: SerpAPIWrapper, **kwargs: Any): + def __init__( + self, + llm: BaseLLM, + search_chain: Union[GoogleSerperAPIWrapper, SerpAPIWrapper], + **kwargs: Any, + ): """Initialize with just an LLM and a search chain.""" search_tool = Tool(name="Intermediate Answer", func=search_chain.run) agent = SelfAskWithSearchAgent.from_llm_and_tools(llm, [search_tool]) diff --git a/langchain/utilities/__init__.py b/langchain/utilities/__init__.py index 0e7eff1d..4598b211 100644 --- a/langchain/utilities/__init__.py +++ b/langchain/utilities/__init__.py @@ -5,6 +5,7 @@ from langchain.serpapi import SerpAPIWrapper 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.wolfram_alpha import WolframAlphaAPIWrapper __all__ = [ @@ -12,6 +13,7 @@ __all__ = [ "RequestsWrapper", "PythonREPL", "GoogleSearchAPIWrapper", + "GoogleSerperAPIWrapper", "WolframAlphaAPIWrapper", "SerpAPIWrapper", "BingSearchAPIWrapper", diff --git a/langchain/utilities/google_serper.py b/langchain/utilities/google_serper.py new file mode 100644 index 00000000..9d8126a3 --- /dev/null +++ b/langchain/utilities/google_serper.py @@ -0,0 +1,94 @@ +"""Util that calls Google Search using the Serper.dev API.""" +from typing import Dict, Optional + +import requests +from pydantic.class_validators import root_validator +from pydantic.main import BaseModel + +from langchain.utils import get_from_dict_or_env + + +class GoogleSerperAPIWrapper(BaseModel): + """Wrapper around the Serper.dev Google Search API. + + You can create a free API key at https://serper.dev. + + To use, you should have the environment variable ``SERPER_API_KEY`` + set with your API key, or pass `serper_api_key` as a named parameter + to the constructor. + + Example: + .. code-block:: python + + from langchain import GoogleSerperAPIWrapper + google_serper = GoogleSerperAPIWrapper() + """ + + k: int = 10 + gl: str = "us" + hl: str = "en" + serper_api_key: Optional[str] = None + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate that api key exists in environment.""" + serper_api_key = get_from_dict_or_env( + values, "serper_api_key", "SERPER_API_KEY" + ) + values["serper_api_key"] = serper_api_key + + return values + + def run(self, query: str) -> str: + """Run query through GoogleSearch and parse result.""" + results = self._google_serper_search_results(query, gl=self.gl, hl=self.hl) + + return self._parse_results(results) + + def _parse_results(self, results: dict) -> str: + snippets = [] + + if results.get("answerBox"): + answer_box = results.get("answerBox", {}) + if answer_box.get("answer"): + return answer_box.get("answer") + elif answer_box.get("snippet"): + return answer_box.get("snippet").replace("\n", " ") + elif answer_box.get("snippetHighlighted"): + return ", ".join(answer_box.get("snippetHighlighted")) + + if results.get("knowledgeGraph"): + kg = results.get("knowledgeGraph", {}) + title = kg.get("title") + entity_type = kg.get("type") + if entity_type: + snippets.append(f"{title}: {entity_type}.") + description = kg.get("description") + if description: + snippets.append(description) + for attribute, value in kg.get("attributes", {}).items(): + snippets.append(f"{title} {attribute}: {value}.") + + for result in results["organic"][: self.k]: + if "snippet" in result: + snippets.append(result["snippet"]) + for attribute, value in result.get("attributes", {}).items(): + snippets.append(f"{attribute}: {value}.") + + if len(snippets) == 0: + return "No good Google Search Result was found" + + return " ".join(snippets) + + def _google_serper_search_results(self, search_term: str, gl: str, hl: str) -> dict: + headers = { + "X-API-KEY": self.serper_api_key or "", + "Content-Type": "application/json", + } + params = {"q": search_term, "gl": gl, "hl": hl} + response = requests.post( + "https://google.serper.dev/search", headers=headers, params=params + ) + response.raise_for_status() + search_results = response.json() + return search_results diff --git a/tests/integration_tests/chains/test_self_ask_with_search.py b/tests/integration_tests/chains/test_self_ask_with_search.py index 3463ec9e..61ef78d9 100644 --- a/tests/integration_tests/chains/test_self_ask_with_search.py +++ b/tests/integration_tests/chains/test_self_ask_with_search.py @@ -1,7 +1,7 @@ """Integration test for self ask with search.""" from langchain.agents.self_ask_with_search.base import SelfAskWithSearchChain from langchain.llms.openai import OpenAI -from langchain.serpapi import SerpAPIWrapper +from langchain.utilities.google_serper import GoogleSerperAPIWrapper def test_self_ask_with_search() -> None: @@ -9,7 +9,7 @@ def test_self_ask_with_search() -> None: question = "What is the hometown of the reigning men's U.S. Open champion?" chain = SelfAskWithSearchChain( llm=OpenAI(temperature=0), - search_chain=SerpAPIWrapper(), + search_chain=GoogleSerperAPIWrapper(), input_key="q", output_key="a", ) diff --git a/tests/integration_tests/test_googleserper_api.py b/tests/integration_tests/test_googleserper_api.py new file mode 100644 index 00000000..67baae4b --- /dev/null +++ b/tests/integration_tests/test_googleserper_api.py @@ -0,0 +1,9 @@ +"""Integration test for Serper.dev's Google Search API Wrapper.""" +from langchain.utilities.google_serper import GoogleSerperAPIWrapper + + +def test_call() -> None: + """Test that call gives the correct answer.""" + search = GoogleSerperAPIWrapper() + output = search.run("What was Obama's first name?") + assert "Barack Hussein Obama II" in output