Skip to content

Commit

Permalink
langchain[minor]: Add StackExchange API integration (#14002)
Browse files Browse the repository at this point in the history
Implements
[#12115](#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 <yuvalkamani@gmail.com>
Co-authored-by: Aryan Thakur <aryanthakur@Aryans-MacBook-Pro.local>
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 <baskaryan@gmail.com>
  • Loading branch information
6 people committed Nov 29, 2023
1 parent d4405bc commit 7ec4dbe
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 0 deletions.
36 changes: 36 additions & 0 deletions docs/docs/integrations/providers/stackexchange.mdx
Original file line number Diff line number Diff line change
@@ -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/).
74 changes: 74 additions & 0 deletions docs/docs/integrations/tools/stackexchange.ipynb
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions libs/langchain/langchain/agents/load_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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.wikipedia.tool import WikipediaQueryRun
from langchain.tools.wolfram_alpha.tool import WolframAlphaQueryRun
from langchain.tools.openweathermap.tool import OpenWeatherMapQueryRun
Expand All @@ -73,6 +74,7 @@
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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -397,6 +403,7 @@ def _get_google_cloud_texttospeech(**kwargs: Any) -> BaseTool:
_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"]),
Expand Down
9 changes: 9 additions & 0 deletions libs/langchain/langchain/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -992,6 +1000,7 @@ def __getattr__(name: str) -> Any:
"ShellTool",
"SleepTool",
"StdInInquireTool",
"StackExchangeTool",
"SteamshipImageGenerationTool",
"StructuredTool",
"Tool",
Expand Down
1 change: 1 addition & 0 deletions libs/langchain/langchain/tools/stackexchange/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""StackExchange API toolkit."""
28 changes: 28 additions & 0 deletions libs/langchain/langchain/tools/stackexchange/tool.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions libs/langchain/langchain/utilities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -326,6 +334,7 @@ def __getattr__(name: str) -> Any:
"SearxSearchWrapper",
"SerpAPIWrapper",
"SparkSQL",
"StackExchangeAPIWrapper",
"TensorflowDatasets",
"TextRequestsWrapper",
"TwilioAPIWrapper",
Expand Down
68 changes: 68 additions & 0 deletions libs/langchain/langchain/utilities/stackexchange.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions libs/langchain/tests/unit_tests/tools/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"SearxSearchRun",
"ShellTool",
"SleepTool",
"StackExchangeTool",
"StdInInquireTool",
"SteamshipImageGenerationTool",
"StructuredTool",
Expand Down
1 change: 1 addition & 0 deletions libs/langchain/tests/unit_tests/tools/test_public_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"ShellTool",
"SleepTool",
"StdInInquireTool",
"StackExchangeTool",
"SteamshipImageGenerationTool",
"StructuredTool",
"Tool",
Expand Down
1 change: 1 addition & 0 deletions libs/langchain/tests/unit_tests/utilities/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"SearxSearchWrapper",
"SerpAPIWrapper",
"SparkSQL",
"StackExchangeAPIWrapper",
"TensorflowDatasets",
"TextRequestsWrapper",
"TwilioAPIWrapper",
Expand Down

0 comments on commit 7ec4dbe

Please sign in to comment.