From 7ec4dbeb802e6ffc1f413c42e2722145a91a6a2c Mon Sep 17 00:00:00 2001 From: Sauhaard <51324450+SauhaardW@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:32:07 -0500 Subject: [PATCH] langchain[minor]: Add StackExchange API integration (#14002) Implements [#12115](https://github.com/langchain-ai/langchain/issues/12115) Who can review? @baskaryan , @eyurtsev , @hwchase17 Integrated Stack Exchange API into Langchain, enabling access to diverse communities within the platform. This addition enhances Langchain's capabilities by allowing users to query Stack Exchange for specialized information and engage in discussions. The integration provides seamless interaction with Stack Exchange content, offering content from varied knowledge repositories. A notebook example and test cases were included to demonstrate the functionality and reliability of this integration. - Add StackExchange as a tool. - Add unit test for the StackExchange wrapper and tool. - Add documentation for the StackExchange wrapper and tool. If you have time, could you please review the code and provide any feedback as necessary! My team is welcome to any suggestions. --------- Co-authored-by: Yuval Kamani Co-authored-by: Aryan Thakur Co-authored-by: Manas1818 <79381912+manas1818@users.noreply.github.com> Co-authored-by: aryan-thakur <61063777+aryan-thakur@users.noreply.github.com> Co-authored-by: Bagatur --- .../integrations/providers/stackexchange.mdx | 36 +++++++++ .../integrations/tools/stackexchange.ipynb | 74 +++++++++++++++++++ libs/langchain/langchain/agents/load_tools.py | 7 ++ libs/langchain/langchain/tools/__init__.py | 9 +++ .../langchain/tools/stackexchange/__init__.py | 1 + .../langchain/tools/stackexchange/tool.py | 28 +++++++ .../langchain/langchain/utilities/__init__.py | 9 +++ .../langchain/utilities/stackexchange.py | 68 +++++++++++++++++ .../utilities/test_stackexchange.py | 23 ++++++ .../tests/unit_tests/tools/test_imports.py | 1 + .../tests/unit_tests/tools/test_public_api.py | 1 + .../unit_tests/utilities/test_imports.py | 1 + 12 files changed, 258 insertions(+) create mode 100644 docs/docs/integrations/providers/stackexchange.mdx create mode 100644 docs/docs/integrations/tools/stackexchange.ipynb create mode 100644 libs/langchain/langchain/tools/stackexchange/__init__.py create mode 100644 libs/langchain/langchain/tools/stackexchange/tool.py create mode 100644 libs/langchain/langchain/utilities/stackexchange.py create mode 100644 libs/langchain/tests/integration_tests/utilities/test_stackexchange.py diff --git a/docs/docs/integrations/providers/stackexchange.mdx b/docs/docs/integrations/providers/stackexchange.mdx new file mode 100644 index 000000000..be31468c1 --- /dev/null +++ b/docs/docs/integrations/providers/stackexchange.mdx @@ -0,0 +1,36 @@ +# Stack Exchange + +>[Stack Exchange](https://en.wikipedia.org/wiki/Stack_Exchange) is a network of +question-and-answer (Q&A) websites on topics in diverse fields, each site covering +a specific topic, where questions, answers, and users are subject to a reputation award process. + +This page covers how to use the `Stack Exchange API` within LangChain. + +## Installation and Setup +- Install requirements with +```bash +pip install stackapi +``` + +## Wrappers + +### Utility + +There exists a StackExchangeAPIWrapper utility which wraps this API. To import this utility: + +```python +from langchain.utilities import StackExchangeAPIWrapper +``` + +For a more detailed walkthrough of this wrapper, see [this notebook](/docs/integrations/tools/stackexchange). + +### 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(["stackexchange"]) +``` + +For more information on tools, see [this page](/docs/modules/agents/tools/). diff --git a/docs/docs/integrations/tools/stackexchange.ipynb b/docs/docs/integrations/tools/stackexchange.ipynb new file mode 100644 index 000000000..dbd00f1b6 --- /dev/null +++ b/docs/docs/integrations/tools/stackexchange.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# StackExchange\n", + "\n", + "This notebook goes over how to use the stack exchange component.\n", + "\n", + "All you need to do is install stackapi:\n", + "1. pip install stackapi\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pip install stackapi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain.utilities import StackExchangeAPIWrapper" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stackexchange = StackExchangeAPIWrapper()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "stackexchange.run(\"zsh: command not found: python\")" + ] + } + ], + "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.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/libs/langchain/langchain/agents/load_tools.py b/libs/langchain/langchain/agents/load_tools.py index 3448a4861..f32cb50f7 100644 --- a/libs/langchain/langchain/agents/load_tools.py +++ b/libs/langchain/langchain/agents/load_tools.py @@ -53,6 +53,7 @@ from langchain.tools.scenexplain.tool import SceneXplainTool 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.wikipedia.tool import WikipediaQueryRun from langchain.tools.wolfram_alpha.tool import WolframAlphaQueryRun from langchain.tools.openweathermap.tool import OpenWeatherMapQueryRun @@ -73,6 +74,7 @@ from langchain.utilities.graphql import GraphQLAPIWrapper from langchain.utilities.searchapi import SearchApiAPIWrapper 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.wikipedia import WikipediaAPIWrapper from langchain.utilities.wolfram_alpha import WolframAlphaAPIWrapper @@ -269,6 +271,10 @@ def _get_serpapi(**kwargs: Any) -> BaseTool: ) +def _get_stackexchange(**kwargs: Any) -> BaseTool: + return StackExchangeTool(api_wrapper=StackExchangeAPIWrapper(**kwargs)) + + def _get_dalle_image_generator(**kwargs: Any) -> Tool: return Tool( "Dall-E-Image-Generator", @@ -397,6 +403,7 @@ _EXTRA_OPTIONAL_TOOLS: Dict[str, Tuple[Callable[[KwArg(Any)], BaseTool], List[st _get_lambda_api, ["awslambda_tool_name", "awslambda_tool_description", "function_name"], ), + "stackexchange": (_get_stackexchange, []), "sceneXplain": (_get_scenexplain, []), "graphql": (_get_graphql_tool, ["graphql_endpoint"]), "openweathermap-api": (_get_openweathermap, ["openweathermap_api_key"]), diff --git a/libs/langchain/langchain/tools/__init__.py b/libs/langchain/langchain/tools/__init__.py index 5718f8dea..7283f9535 100644 --- a/libs/langchain/langchain/tools/__init__.py +++ b/libs/langchain/langchain/tools/__init__.py @@ -612,6 +612,12 @@ def _import_sql_database_tool_QuerySQLDataBaseTool() -> Any: return QuerySQLDataBaseTool +def _import_stackexchange_tool() -> Any: + from langchain.tools.stackexchange.tool import StackExchangeTool + + return StackExchangeTool + + def _import_steamship_image_generation() -> Any: from langchain.tools.steamship_image_generation import SteamshipImageGenerationTool @@ -871,6 +877,8 @@ def __getattr__(name: str) -> Any: return _import_sql_database_tool_QuerySQLCheckerTool() elif name == "QuerySQLDataBaseTool": return _import_sql_database_tool_QuerySQLDataBaseTool() + elif name == "StackExchangeTool": + return _import_stackexchange_tool() elif name == "SteamshipImageGenerationTool": return _import_steamship_image_generation() elif name == "VectorStoreQATool": @@ -992,6 +1000,7 @@ __all__ = [ "ShellTool", "SleepTool", "StdInInquireTool", + "StackExchangeTool", "SteamshipImageGenerationTool", "StructuredTool", "Tool", diff --git a/libs/langchain/langchain/tools/stackexchange/__init__.py b/libs/langchain/langchain/tools/stackexchange/__init__.py new file mode 100644 index 000000000..1fa9a483d --- /dev/null +++ b/libs/langchain/langchain/tools/stackexchange/__init__.py @@ -0,0 +1 @@ +"""StackExchange API toolkit.""" diff --git a/libs/langchain/langchain/tools/stackexchange/tool.py b/libs/langchain/langchain/tools/stackexchange/tool.py new file mode 100644 index 000000000..f35a44da4 --- /dev/null +++ b/libs/langchain/langchain/tools/stackexchange/tool.py @@ -0,0 +1,28 @@ +"""Tool for the Wikipedia API.""" + +from typing import Optional + +from langchain.callbacks.manager import CallbackManagerForToolRun +from langchain.tools.base import BaseTool +from langchain.utilities.stackexchange import StackExchangeAPIWrapper + + +class StackExchangeTool(BaseTool): + """Tool that uses StackExchange""" + + name: str = "StackExchange" + description: str = ( + "A wrapper around StackExchange. " + "Useful for when you need to answer specific programming questions" + "code excerpts, code examples and solutions" + "Input should be a fully formed question." + ) + api_wrapper: StackExchangeAPIWrapper + + def _run( + self, + query: str, + run_manager: Optional[CallbackManagerForToolRun] = None, + ) -> str: + """Use the Stack Exchange tool.""" + return self.api_wrapper.run(query) diff --git a/libs/langchain/langchain/utilities/__init__.py b/libs/langchain/langchain/utilities/__init__.py index 165a2d934..865144f04 100644 --- a/libs/langchain/langchain/utilities/__init__.py +++ b/libs/langchain/langchain/utilities/__init__.py @@ -188,6 +188,12 @@ def _import_sql_database() -> Any: return SQLDatabase +def _import_stackexchange() -> Any: + from langchain.utilities.stackexchange import StackExchangeAPIWrapper + + return StackExchangeAPIWrapper + + def _import_tensorflow_datasets() -> Any: from langchain.utilities.tensorflow_datasets import TensorflowDatasets @@ -277,6 +283,8 @@ def __getattr__(name: str) -> Any: return _import_serpapi() elif name == "SparkSQL": return _import_spark_sql() + elif name == "StackExchangeAPIWrapper": + return _import_stackexchange() elif name == "SQLDatabase": return _import_sql_database() elif name == "TensorflowDatasets": @@ -326,6 +334,7 @@ __all__ = [ "SearxSearchWrapper", "SerpAPIWrapper", "SparkSQL", + "StackExchangeAPIWrapper", "TensorflowDatasets", "TextRequestsWrapper", "TwilioAPIWrapper", diff --git a/libs/langchain/langchain/utilities/stackexchange.py b/libs/langchain/langchain/utilities/stackexchange.py new file mode 100644 index 000000000..2c9bd2d96 --- /dev/null +++ b/libs/langchain/langchain/utilities/stackexchange.py @@ -0,0 +1,68 @@ +import html +from typing import Any, Dict, Literal + +from langchain.pydantic_v1 import BaseModel, Field, root_validator + + +class StackExchangeAPIWrapper(BaseModel): + """Wrapper for Stack Exchange API.""" + + client: Any #: :meta private: + max_results: int = 3 + """Max number of results to include in output.""" + query_type: Literal["all", "title", "body"] = "all" + """Which part of StackOverflows items to match against. One of 'all', 'title', + 'body'. Defaults to 'all'. + """ + fetch_params: Dict[str, Any] = Field(default_factory=dict) + """Additional params to pass to StackApi.fetch.""" + result_separator: str = "\n\n" + """Separator between question,answer pairs.""" + + @root_validator() + def validate_environment(cls, values: Dict) -> Dict: + """Validate that the required Python package exists.""" + try: + from stackapi import StackAPI + + values["client"] = StackAPI("stackoverflow") + except ImportError: + raise ImportError( + "The 'stackapi' Python package is not installed. " + "Please install it with `pip install stackapi`." + ) + return values + + def run(self, query: str) -> str: + """Run query through StackExchange API and parse results.""" + + query_key = "q" if self.query_type == "all" else self.query_type + output = self.client.fetch( + "search/excerpts", **{query_key: query}, **self.fetch_params + ) + if len(output["items"]) < 1: + return f"No relevant results found for '{query}' on Stack Overflow." + questions = [ + item for item in output["items"] if item["item_type"] == "question" + ][: self.max_results] + answers = [item for item in output["items"] if item["item_type"] == "answer"] + results = [] + for question in questions: + res_text = f"Question: {question['title']}\n{question['excerpt']}" + relevant_answers = [ + answer + for answer in answers + if answer["question_id"] == question["question_id"] + ] + accepted_answers = [ + answer for answer in relevant_answers if answer["is_accepted"] + ] + if relevant_answers: + top_answer = ( + accepted_answers[0] if accepted_answers else relevant_answers[0] + ) + excerpt = html.unescape(top_answer["excerpt"]) + res_text += f"\nAnswer: {excerpt}" + results.append(res_text) + + return self.result_separator.join(results) diff --git a/libs/langchain/tests/integration_tests/utilities/test_stackexchange.py b/libs/langchain/tests/integration_tests/utilities/test_stackexchange.py new file mode 100644 index 000000000..634742c43 --- /dev/null +++ b/libs/langchain/tests/integration_tests/utilities/test_stackexchange.py @@ -0,0 +1,23 @@ +"""Integration test for Stack Exchange.""" +from langchain.utilities import StackExchangeAPIWrapper + + +def test_call() -> None: + """Test that call runs.""" + stackexchange = StackExchangeAPIWrapper() + output = stackexchange.run("zsh: command not found: python") + assert output != "hello" + + +def test_failure() -> None: + """Test that call that doesn't run.""" + stackexchange = StackExchangeAPIWrapper() + output = stackexchange.run("sjefbsmnf") + assert output == "No relevant results found for 'sjefbsmnf' on Stack Overflow" + + +def test_success() -> None: + """Test that call that doesn't run.""" + stackexchange = StackExchangeAPIWrapper() + output = stackexchange.run("zsh: command not found: python") + assert "zsh: command not found: python" 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 3b251fcfe..bf613e3aa 100644 --- a/libs/langchain/tests/unit_tests/tools/test_imports.py +++ b/libs/langchain/tests/unit_tests/tools/test_imports.py @@ -94,6 +94,7 @@ EXPECTED_ALL = [ "SearxSearchRun", "ShellTool", "SleepTool", + "StackExchangeTool", "StdInInquireTool", "SteamshipImageGenerationTool", "StructuredTool", 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 d9e08e664..a0e51bdfc 100644 --- a/libs/langchain/tests/unit_tests/tools/test_public_api.py +++ b/libs/langchain/tests/unit_tests/tools/test_public_api.py @@ -96,6 +96,7 @@ _EXPECTED = [ "ShellTool", "SleepTool", "StdInInquireTool", + "StackExchangeTool", "SteamshipImageGenerationTool", "StructuredTool", "Tool", diff --git a/libs/langchain/tests/unit_tests/utilities/test_imports.py b/libs/langchain/tests/unit_tests/utilities/test_imports.py index 1924e3d29..baf9f9fe5 100644 --- a/libs/langchain/tests/unit_tests/utilities/test_imports.py +++ b/libs/langchain/tests/unit_tests/utilities/test_imports.py @@ -33,6 +33,7 @@ EXPECTED_ALL = [ "SearxSearchWrapper", "SerpAPIWrapper", "SparkSQL", + "StackExchangeAPIWrapper", "TensorflowDatasets", "TextRequestsWrapper", "TwilioAPIWrapper",