langchain-crustapi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain_crustapi-0.1.0/LICENSE +21 -0
- langchain_crustapi-0.1.0/PKG-INFO +70 -0
- langchain_crustapi-0.1.0/README.md +47 -0
- langchain_crustapi-0.1.0/langchain_crustapi/__init__.py +17 -0
- langchain_crustapi-0.1.0/langchain_crustapi/_utilities.py +109 -0
- langchain_crustapi-0.1.0/langchain_crustapi/crustapi_search.py +126 -0
- langchain_crustapi-0.1.0/langchain_crustapi/py.typed +0 -0
- langchain_crustapi-0.1.0/pyproject.toml +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 COLDWIRES LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-crustapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An integration package connecting CrustAPI (Google search data) and LangChain
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: langchain,crustapi,google,search,serp,maps,tools
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: aiohttp (>=3.9,<4.0)
|
|
18
|
+
Requires-Dist: langchain-core (>=0.3,<2.0)
|
|
19
|
+
Requires-Dist: requests (>=2.32,<3.0)
|
|
20
|
+
Project-URL: Repository, https://github.com/crustapi/langchain-crustapi
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# langchain-crustapi
|
|
24
|
+
|
|
25
|
+
This package connects [CrustAPI](https://crustapi.com) to LangChain, giving your chains and agents clean, structured Google data from one endpoint: web search, Maps, News, Shopping, Images, Videos, Places, Scholar, and Patents. You only pay for successful results, and there's a free tier with no card.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -U langchain-crustapi
|
|
31
|
+
export CRUSTAPI_API_KEY="your-key" # free at https://crustapi.com
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from langchain_crustapi import CrustAPISearch
|
|
38
|
+
|
|
39
|
+
# Web search (default)
|
|
40
|
+
tool = CrustAPISearch(max_results=5)
|
|
41
|
+
tool.invoke({"query": "best crm software"})
|
|
42
|
+
|
|
43
|
+
# Pick a Google surface
|
|
44
|
+
maps = CrustAPISearch(search_type="maps", location="Austin, TX")
|
|
45
|
+
maps.invoke({"query": "dentists"})
|
|
46
|
+
|
|
47
|
+
news = CrustAPISearch(search_type="news")
|
|
48
|
+
news.invoke({"query": "openai"})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Bind it to an agent like any other LangChain tool:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from langchain.chat_models import init_chat_model
|
|
55
|
+
from langchain_crustapi import CrustAPISearch
|
|
56
|
+
|
|
57
|
+
llm = init_chat_model("gpt-4o-mini")
|
|
58
|
+
agent_llm = llm.bind_tools([CrustAPISearch()])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Parameters
|
|
62
|
+
|
|
63
|
+
- `search_type`: the Google surface — `web` (default), `news`, `maps`, `places`, `shopping`, `images`, `videos`, `scholar`, `patents`.
|
|
64
|
+
- `max_results`: number of results to return (default `10`).
|
|
65
|
+
- `gl`: two-letter country code, e.g. `us`.
|
|
66
|
+
- `hl`: two-letter language code, e.g. `en`.
|
|
67
|
+
- `location`: city or region for local results, e.g. `Austin, TX`.
|
|
68
|
+
|
|
69
|
+
The API key is read from the `CRUSTAPI_API_KEY` environment variable, or you can pass `crustapi_api_key` when constructing the tool.
|
|
70
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# langchain-crustapi
|
|
2
|
+
|
|
3
|
+
This package connects [CrustAPI](https://crustapi.com) to LangChain, giving your chains and agents clean, structured Google data from one endpoint: web search, Maps, News, Shopping, Images, Videos, Places, Scholar, and Patents. You only pay for successful results, and there's a free tier with no card.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -U langchain-crustapi
|
|
9
|
+
export CRUSTAPI_API_KEY="your-key" # free at https://crustapi.com
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from langchain_crustapi import CrustAPISearch
|
|
16
|
+
|
|
17
|
+
# Web search (default)
|
|
18
|
+
tool = CrustAPISearch(max_results=5)
|
|
19
|
+
tool.invoke({"query": "best crm software"})
|
|
20
|
+
|
|
21
|
+
# Pick a Google surface
|
|
22
|
+
maps = CrustAPISearch(search_type="maps", location="Austin, TX")
|
|
23
|
+
maps.invoke({"query": "dentists"})
|
|
24
|
+
|
|
25
|
+
news = CrustAPISearch(search_type="news")
|
|
26
|
+
news.invoke({"query": "openai"})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Bind it to an agent like any other LangChain tool:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from langchain.chat_models import init_chat_model
|
|
33
|
+
from langchain_crustapi import CrustAPISearch
|
|
34
|
+
|
|
35
|
+
llm = init_chat_model("gpt-4o-mini")
|
|
36
|
+
agent_llm = llm.bind_tools([CrustAPISearch()])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Parameters
|
|
40
|
+
|
|
41
|
+
- `search_type`: the Google surface — `web` (default), `news`, `maps`, `places`, `shopping`, `images`, `videos`, `scholar`, `patents`.
|
|
42
|
+
- `max_results`: number of results to return (default `10`).
|
|
43
|
+
- `gl`: two-letter country code, e.g. `us`.
|
|
44
|
+
- `hl`: two-letter language code, e.g. `en`.
|
|
45
|
+
- `location`: city or region for local results, e.g. `Austin, TX`.
|
|
46
|
+
|
|
47
|
+
The API key is read from the `CRUSTAPI_API_KEY` environment variable, or you can pass `crustapi_api_key` when constructing the tool.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from importlib import metadata
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from langchain_crustapi.crustapi_search import CrustAPISearch
|
|
5
|
+
from langchain_crustapi._utilities import CrustAPISearchAPIWrapper
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__: str = metadata.version(__package__)
|
|
9
|
+
except metadata.PackageNotFoundError:
|
|
10
|
+
__version__ = ""
|
|
11
|
+
del metadata
|
|
12
|
+
|
|
13
|
+
__all__: List[str] = [
|
|
14
|
+
"CrustAPISearch",
|
|
15
|
+
"CrustAPISearchAPIWrapper",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Utility wrapper for the CrustAPI Google search endpoint."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
import requests
|
|
7
|
+
from langchain_core.utils import get_from_dict_or_env
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, SecretStr, model_validator
|
|
9
|
+
|
|
10
|
+
CRUSTAPI_URL = "https://crustapi.com/v1/search"
|
|
11
|
+
|
|
12
|
+
# Google surfaces CrustAPI exposes through the single search endpoint.
|
|
13
|
+
SEARCH_TYPES = (
|
|
14
|
+
"web",
|
|
15
|
+
"news",
|
|
16
|
+
"maps",
|
|
17
|
+
"places",
|
|
18
|
+
"shopping",
|
|
19
|
+
"images",
|
|
20
|
+
"videos",
|
|
21
|
+
"scholar",
|
|
22
|
+
"patents",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CrustAPISearchAPIWrapper(BaseModel):
|
|
27
|
+
"""Wrapper around the CrustAPI search endpoint.
|
|
28
|
+
|
|
29
|
+
Reads the API key from the ``crustapi_api_key`` argument or the
|
|
30
|
+
``CRUSTAPI_API_KEY`` environment variable. Get a free key at
|
|
31
|
+
https://crustapi.com (3,000 credits per month, no card).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
crustapi_api_key: SecretStr
|
|
35
|
+
api_base_url: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
@model_validator(mode="before")
|
|
40
|
+
@classmethod
|
|
41
|
+
def validate_environment(cls, values: Dict) -> Any:
|
|
42
|
+
"""Validate that the API key exists in the arguments or environment."""
|
|
43
|
+
values["crustapi_api_key"] = get_from_dict_or_env(
|
|
44
|
+
values, "crustapi_api_key", "CRUSTAPI_API_KEY"
|
|
45
|
+
)
|
|
46
|
+
return values
|
|
47
|
+
|
|
48
|
+
def _build_params(
|
|
49
|
+
self,
|
|
50
|
+
query: str,
|
|
51
|
+
search_type: str,
|
|
52
|
+
num: int,
|
|
53
|
+
gl: Optional[str],
|
|
54
|
+
hl: Optional[str],
|
|
55
|
+
location: Optional[str],
|
|
56
|
+
) -> Dict[str, Any]:
|
|
57
|
+
if search_type not in SEARCH_TYPES:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"Invalid search_type: {search_type}. "
|
|
60
|
+
f"Must be one of: {', '.join(SEARCH_TYPES)}"
|
|
61
|
+
)
|
|
62
|
+
params: Dict[str, Any] = {"type": search_type, "q": query, "num": num}
|
|
63
|
+
if gl:
|
|
64
|
+
params["gl"] = gl
|
|
65
|
+
if hl:
|
|
66
|
+
params["hl"] = hl
|
|
67
|
+
if location:
|
|
68
|
+
params["location"] = location
|
|
69
|
+
return params
|
|
70
|
+
|
|
71
|
+
def raw_results(
|
|
72
|
+
self,
|
|
73
|
+
query: str,
|
|
74
|
+
search_type: str = "web",
|
|
75
|
+
num: int = 10,
|
|
76
|
+
gl: Optional[str] = None,
|
|
77
|
+
hl: Optional[str] = None,
|
|
78
|
+
location: Optional[str] = None,
|
|
79
|
+
) -> Dict:
|
|
80
|
+
"""Run a search and return the raw JSON response."""
|
|
81
|
+
params = self._build_params(query, search_type, num, gl, hl, location)
|
|
82
|
+
headers = {"x-api-key": self.crustapi_api_key.get_secret_value()}
|
|
83
|
+
base_url = self.api_base_url or CRUSTAPI_URL
|
|
84
|
+
response = requests.get(base_url, params=params, headers=headers, timeout=30)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
return response.json()
|
|
87
|
+
|
|
88
|
+
async def raw_results_async(
|
|
89
|
+
self,
|
|
90
|
+
query: str,
|
|
91
|
+
search_type: str = "web",
|
|
92
|
+
num: int = 10,
|
|
93
|
+
gl: Optional[str] = None,
|
|
94
|
+
hl: Optional[str] = None,
|
|
95
|
+
location: Optional[str] = None,
|
|
96
|
+
) -> Dict:
|
|
97
|
+
"""Run a search asynchronously and return the raw JSON response."""
|
|
98
|
+
params = self._build_params(query, search_type, num, gl, hl, location)
|
|
99
|
+
headers = {"x-api-key": self.crustapi_api_key.get_secret_value()}
|
|
100
|
+
base_url = self.api_base_url or CRUSTAPI_URL
|
|
101
|
+
async with aiohttp.ClientSession() as session:
|
|
102
|
+
async with session.get(
|
|
103
|
+
base_url,
|
|
104
|
+
params=params,
|
|
105
|
+
headers=headers,
|
|
106
|
+
timeout=aiohttp.ClientTimeout(total=30),
|
|
107
|
+
) as response:
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
return await response.json()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""CrustAPI Google search tool for LangChain."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Literal, Optional, Type
|
|
4
|
+
|
|
5
|
+
from langchain_core.callbacks import (
|
|
6
|
+
AsyncCallbackManagerForToolRun,
|
|
7
|
+
CallbackManagerForToolRun,
|
|
8
|
+
)
|
|
9
|
+
from langchain_core.tools import BaseTool
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from langchain_crustapi._utilities import CrustAPISearchAPIWrapper
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CrustAPISearchInput(BaseModel):
|
|
16
|
+
"""Input for the CrustAPI search tool."""
|
|
17
|
+
|
|
18
|
+
query: str = Field(description="Search query to look up on Google")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CrustAPISearch(BaseTool): # type: ignore[override]
|
|
22
|
+
"""Search Google through CrustAPI and get clean, structured JSON.
|
|
23
|
+
|
|
24
|
+
One tool covers multiple Google surfaces via ``search_type``: ``web``
|
|
25
|
+
(default), ``news``, ``maps``, ``places``, ``shopping``, ``images``,
|
|
26
|
+
``videos``, ``scholar``, and ``patents``. You only pay for successful
|
|
27
|
+
results, and there is a free tier with no card.
|
|
28
|
+
|
|
29
|
+
Setup:
|
|
30
|
+
Install ``langchain-crustapi`` and set the ``CRUSTAPI_API_KEY``
|
|
31
|
+
environment variable with a key from https://crustapi.com.
|
|
32
|
+
|
|
33
|
+
.. code-block:: bash
|
|
34
|
+
|
|
35
|
+
pip install langchain-crustapi
|
|
36
|
+
export CRUSTAPI_API_KEY="your-key"
|
|
37
|
+
|
|
38
|
+
Instantiate:
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
from langchain_crustapi import CrustAPISearch
|
|
42
|
+
|
|
43
|
+
tool = CrustAPISearch(max_results=5)
|
|
44
|
+
|
|
45
|
+
Invoke:
|
|
46
|
+
.. code-block:: python
|
|
47
|
+
|
|
48
|
+
tool.invoke({"query": "best crm software"})
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str = "crustapi_search"
|
|
52
|
+
description: str = (
|
|
53
|
+
"Search Google via CrustAPI and return clean, structured JSON. "
|
|
54
|
+
"Useful for current web results, local businesses (maps), news, "
|
|
55
|
+
"shopping, images, videos, scholar, and patents. Input is a search query."
|
|
56
|
+
)
|
|
57
|
+
args_schema: Type[BaseModel] = CrustAPISearchInput
|
|
58
|
+
|
|
59
|
+
search_type: Literal[
|
|
60
|
+
"web",
|
|
61
|
+
"news",
|
|
62
|
+
"maps",
|
|
63
|
+
"places",
|
|
64
|
+
"shopping",
|
|
65
|
+
"images",
|
|
66
|
+
"videos",
|
|
67
|
+
"scholar",
|
|
68
|
+
"patents",
|
|
69
|
+
] = "web"
|
|
70
|
+
"""Which Google surface to query."""
|
|
71
|
+
|
|
72
|
+
max_results: int = 10
|
|
73
|
+
"""Maximum number of results to return."""
|
|
74
|
+
|
|
75
|
+
gl: Optional[str] = None
|
|
76
|
+
"""Two-letter country code, e.g. ``us``."""
|
|
77
|
+
|
|
78
|
+
hl: Optional[str] = None
|
|
79
|
+
"""Two-letter language code, e.g. ``en``."""
|
|
80
|
+
|
|
81
|
+
location: Optional[str] = None
|
|
82
|
+
"""City or region for local results, e.g. ``Austin, TX``."""
|
|
83
|
+
|
|
84
|
+
api_wrapper: CrustAPISearchAPIWrapper = Field(default_factory=CrustAPISearchAPIWrapper) # type: ignore[arg-type]
|
|
85
|
+
|
|
86
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
87
|
+
# Let callers pass crustapi_api_key / api_base_url straight through to the wrapper.
|
|
88
|
+
if "api_wrapper" not in kwargs:
|
|
89
|
+
wrapper_kwargs = {}
|
|
90
|
+
if "crustapi_api_key" in kwargs:
|
|
91
|
+
wrapper_kwargs["crustapi_api_key"] = kwargs.pop("crustapi_api_key")
|
|
92
|
+
if "api_base_url" in kwargs:
|
|
93
|
+
wrapper_kwargs["api_base_url"] = kwargs.pop("api_base_url")
|
|
94
|
+
if wrapper_kwargs:
|
|
95
|
+
kwargs["api_wrapper"] = CrustAPISearchAPIWrapper(**wrapper_kwargs)
|
|
96
|
+
super().__init__(**kwargs)
|
|
97
|
+
|
|
98
|
+
def _run(
|
|
99
|
+
self,
|
|
100
|
+
query: str,
|
|
101
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
102
|
+
) -> Dict[str, Any]:
|
|
103
|
+
"""Run the search."""
|
|
104
|
+
return self.api_wrapper.raw_results(
|
|
105
|
+
query=query,
|
|
106
|
+
search_type=self.search_type,
|
|
107
|
+
num=self.max_results,
|
|
108
|
+
gl=self.gl,
|
|
109
|
+
hl=self.hl,
|
|
110
|
+
location=self.location,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def _arun(
|
|
114
|
+
self,
|
|
115
|
+
query: str,
|
|
116
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
117
|
+
) -> Dict[str, Any]:
|
|
118
|
+
"""Run the search asynchronously."""
|
|
119
|
+
return await self.api_wrapper.raw_results_async(
|
|
120
|
+
query=query,
|
|
121
|
+
search_type=self.search_type,
|
|
122
|
+
num=self.max_results,
|
|
123
|
+
gl=self.gl,
|
|
124
|
+
hl=self.hl,
|
|
125
|
+
location=self.location,
|
|
126
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "langchain-crustapi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "An integration package connecting CrustAPI (Google search data) and LangChain"
|
|
9
|
+
authors = []
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
repository = "https://github.com/crustapi/langchain-crustapi"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
keywords = ["langchain", "crustapi", "google", "search", "serp", "maps", "tools"]
|
|
14
|
+
include = ["langchain_crustapi/py.typed"]
|
|
15
|
+
|
|
16
|
+
[tool.poetry.dependencies]
|
|
17
|
+
python = ">=3.9,<4.0"
|
|
18
|
+
langchain-core = ">=0.3,<2.0"
|
|
19
|
+
aiohttp = "^3.9"
|
|
20
|
+
requests = "^2.32"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.group.test]
|
|
23
|
+
optional = true
|
|
24
|
+
|
|
25
|
+
[tool.poetry.group.test.dependencies]
|
|
26
|
+
pytest = "^8.0"
|
|
27
|
+
pytest-asyncio = "^0.23"
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
asyncio_mode = "auto"
|