diff --git a/docs/docs/integrations/providers/stackexchange.mdx b/docs/docs/integrations/providers/stackexchange.mdx new file mode 100644 index 00000000000..be31468c14c --- /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 00000000000..dbd00f1b64e --- /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 3448a4861d9..f32cb50f7e3 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 5718f8dea1a..7283f953548 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 00000000000..1fa9a483d10 --- /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 00000000000..f35a44da4aa --- /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 165a2d9341c..865144f0458 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 00000000000..2c9bd2d96da --- /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 00000000000..634742c4342 --- /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 3b251fcfe1f..bf613e3aae6 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 d9e08e664b3..a0e51bdfc22 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 1924e3d29b5..baf9f9fe5b8 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",