langchain-scrapeunblocker 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_scrapeunblocker-0.1.0/.github/workflows/publish.yml +24 -0
- langchain_scrapeunblocker-0.1.0/.github/workflows/test.yml +20 -0
- langchain_scrapeunblocker-0.1.0/.gitignore +7 -0
- langchain_scrapeunblocker-0.1.0/CHANGELOG.md +5 -0
- langchain_scrapeunblocker-0.1.0/CLAUDE.md +17 -0
- langchain_scrapeunblocker-0.1.0/LICENSE +21 -0
- langchain_scrapeunblocker-0.1.0/PKG-INFO +129 -0
- langchain_scrapeunblocker-0.1.0/README.md +101 -0
- langchain_scrapeunblocker-0.1.0/pyproject.toml +43 -0
- langchain_scrapeunblocker-0.1.0/src/langchain_scrapeunblocker/__init__.py +27 -0
- langchain_scrapeunblocker-0.1.0/src/langchain_scrapeunblocker/_base.py +139 -0
- langchain_scrapeunblocker-0.1.0/src/langchain_scrapeunblocker/_catalog.py +106 -0
- langchain_scrapeunblocker-0.1.0/src/langchain_scrapeunblocker/toolkit.py +65 -0
- langchain_scrapeunblocker-0.1.0/src/langchain_scrapeunblocker/tools.py +531 -0
- langchain_scrapeunblocker-0.1.0/src/langchain_scrapeunblocker/version.py +1 -0
- langchain_scrapeunblocker-0.1.0/tests/integration_tests/test_standard.py +82 -0
- langchain_scrapeunblocker-0.1.0/tests/unit_tests/test_standard.py +85 -0
- langchain_scrapeunblocker-0.1.0/tests/unit_tests/test_tools.py +278 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes on every GitHub Release through PyPI Trusted Publishing (OIDC):
|
|
4
|
+
# no API token anywhere. The trusted publisher points at this repo, this
|
|
5
|
+
# workflow (publish.yml) and the environment "pypi".
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment: pypi
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- run: pip install -e ".[test]" && pytest
|
|
23
|
+
- run: pip install build && python -m build
|
|
24
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e ".[test]"
|
|
20
|
+
- run: pytest -v
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First release: `ScrapeUnblockerMarkdown`, `ScrapeUnblockerSearchAndRead`, `ScrapeUnblockerGoogleSearch`, `ScrapeUnblockerParsedData`, `ScrapeUnblockerListPlugins`, `ScrapeUnblockerRunPlugin` and `ScrapeUnblockerToolkit`, sync and async, passing LangChain's standard tool tests.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- HQ-SESSION-GUARD v1 (managed from ScrapeUnblocker/headquarters - do not remove) -->
|
|
2
|
+
# HQ SESSION GUARD (Claude - MANDATORY, READ FIRST)
|
|
3
|
+
|
|
4
|
+
This repo is part of the ScrapeUnblocker ecosystem. All development on it is governed by the central
|
|
5
|
+
`headquarters` repo (`ScrapeUnblocker/headquarters`) - its CLAUDE.md carries the global rules
|
|
6
|
+
(branch/push policy, lint gates, credentials handling, worklog, deploy runbooks).
|
|
7
|
+
|
|
8
|
+
- **If the headquarters CLAUDE.md IS loaded in your context** (the session was started from the local
|
|
9
|
+
`headquarters` folder - you can see its "ScrapeUnblocker - Valdymo pultas (Headquarters)" instructions),
|
|
10
|
+
this guard is satisfied: work normally under those rules.
|
|
11
|
+
- **If it is NOT loaded** (Claude was launched directly in this repo or anywhere else): treat this repo as
|
|
12
|
+
**READ-ONLY**. Do NOT edit files, do NOT commit, do NOT push, do NOT create branches or tags, and do NOT
|
|
13
|
+
run deploys from here. Tell the developer: ScrapeUnblocker development sessions must be started from the
|
|
14
|
+
local `headquarters` folder (clone of `ScrapeUnblocker/headquarters`) so the global rules load - then stop.
|
|
15
|
+
- Reading, searching, running read-only commands and explaining code is always allowed.
|
|
16
|
+
- **Exemption:** sanctioned headless server agents (self-heal / scout / parts-monitor / no-code crawler etc.
|
|
17
|
+
running on our servers) follow their own playbooks and are NOT bound by this guard.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScrapeUnblocker
|
|
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,129 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-scrapeunblocker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools for ScrapeUnblocker: read any web page as Markdown or JSON, search Google, run 100+ site scrapers
|
|
5
|
+
Project-URL: Homepage, https://scrapeunblocker.com?utm_source=pypi&utm_medium=integration&utm_campaign=langchain
|
|
6
|
+
Project-URL: Documentation, https://github.com/ScrapeUnblocker/langchain-scrapeunblocker#readme
|
|
7
|
+
Project-URL: Source, https://github.com/ScrapeUnblocker/langchain-scrapeunblocker
|
|
8
|
+
Project-URL: Issues, https://github.com/ScrapeUnblocker/langchain-scrapeunblocker/issues
|
|
9
|
+
Author-email: ScrapeUnblocker <info@scrapeunblocker.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,langchain,llm,markdown,scraping,serp,tools,web-scraping
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: langchain-core<2.0.0,>=0.3.0
|
|
22
|
+
Requires-Dist: scrapeunblocker>=0.18.6
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: langchain-tests==1.1.9; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# langchain-scrapeunblocker
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/langchain-scrapeunblocker/)
|
|
32
|
+
[](https://github.com/ScrapeUnblocker/langchain-scrapeunblocker/actions/workflows/test.yml)
|
|
33
|
+
|
|
34
|
+
LangChain tools for [ScrapeUnblocker](https://scrapeunblocker.com?utm_source=github&utm_medium=integration&utm_campaign=langchain): give your agent live web data. Read any public page in a real browser as clean Markdown or structured JSON, search Google, and run 100+ ready-made site scrapers (Amazon, Walmart, eBay, Google Maps, YouTube, Zillow, Indeed and more), including pages behind Cloudflare, DataDome, PerimeterX and Akamai.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -U langchain-scrapeunblocker
|
|
40
|
+
export SCRAPEUNBLOCKER_API_KEY="your-api-key"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Get a key at [app.scrapeunblocker.com](https://app.scrapeunblocker.com?utm_source=github&utm_medium=integration&utm_campaign=langchain) - a free account comes with 500 requests to start.
|
|
44
|
+
|
|
45
|
+
## Use with an agent
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from langchain.agents import create_agent
|
|
49
|
+
from langchain_scrapeunblocker import ScrapeUnblockerToolkit
|
|
50
|
+
|
|
51
|
+
agent = create_agent(
|
|
52
|
+
"anthropic:claude-sonnet-5-5",
|
|
53
|
+
tools=ScrapeUnblockerToolkit().get_tools(),
|
|
54
|
+
)
|
|
55
|
+
result = agent.invoke(
|
|
56
|
+
{"messages": [{"role": "user", "content": "What does the Echo Dot cost on amazon.com today?"}]}
|
|
57
|
+
)
|
|
58
|
+
print(result["messages"][-1].content)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Tools
|
|
62
|
+
|
|
63
|
+
| Tool | Name the model sees | What it does | Billed |
|
|
64
|
+
|------|---------------------|--------------|--------|
|
|
65
|
+
| `ScrapeUnblockerMarkdown` | `scrapeunblocker_markdown` | A page as clean Markdown (main content, absolute links) | 1 request |
|
|
66
|
+
| `ScrapeUnblockerSearchAndRead` | `scrapeunblocker_search_and_read` | Google search plus the top 1-5 results read as Markdown, in one call | 1 + 1 per page |
|
|
67
|
+
| `ScrapeUnblockerGoogleSearch` | `scrapeunblocker_google_search` | Organic results, AI Overview, People also ask, related searches | 1 per result page |
|
|
68
|
+
| `ScrapeUnblockerParsedData` | `scrapeunblocker_parsed_data` | Structured JSON from a product, article or listing page | 1 request |
|
|
69
|
+
| `ScrapeUnblockerListPlugins` | `scrapeunblocker_list_plugins` | Search the catalog of 100+ site scrapers | free |
|
|
70
|
+
| `ScrapeUnblockerRunPlugin` | `scrapeunblocker_run_plugin` | Run a site scraper and get its JSON | per plugin |
|
|
71
|
+
|
|
72
|
+
Use them one by one:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from langchain_scrapeunblocker import ScrapeUnblockerMarkdown, ScrapeUnblockerRunPlugin
|
|
76
|
+
|
|
77
|
+
ScrapeUnblockerMarkdown().invoke({"url": "https://example.com"})
|
|
78
|
+
|
|
79
|
+
ScrapeUnblockerRunPlugin().invoke(
|
|
80
|
+
{"plugin": "youtube_transcript", "params": {"video": "dQw4w9WgXcQ"}}
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`ScrapeUnblockerGoogleSearch`, `ScrapeUnblockerParsedData` and `ScrapeUnblockerRunPlugin` return `content_and_artifact`: the model gets a compact text or JSON, and the full API response is on the `ToolMessage`'s `artifact` for your code. Called with a tool call, they return that message:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from langchain_scrapeunblocker import ScrapeUnblockerGoogleSearch
|
|
88
|
+
|
|
89
|
+
msg = ScrapeUnblockerGoogleSearch().invoke(
|
|
90
|
+
{"name": "scrapeunblocker_google_search", "args": {"query": "langchain"}, "id": "1", "type": "tool_call"}
|
|
91
|
+
)
|
|
92
|
+
msg.content # what the model reads
|
|
93
|
+
msg.artifact # the whole results page: ads, knowledge panel, local, shopping, videos...
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Every tool takes an optional `proxy_country` (ISO code such as `"US"` or `"DE"`) to make the request from that country, and works with `ainvoke` too.
|
|
97
|
+
|
|
98
|
+
The plugin catalog comes from the installed [`scrapeunblocker`](https://pypi.org/project/scrapeunblocker/) SDK: `pip install -U scrapeunblocker` picks up new plugins without a new release of this package.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Argument | Default | |
|
|
103
|
+
|----------|---------|---|
|
|
104
|
+
| `api_key` | `SCRAPEUNBLOCKER_API_KEY` (or `SCRAPEUNBLOCKER_KEY`) | Your API key |
|
|
105
|
+
| `timeout` | `180` | Seconds per API call; protected pages can take a while |
|
|
106
|
+
| `max_retries` | `2` | Retries for 429 and 502-504 |
|
|
107
|
+
| `max_chars` | `100000` (Markdown), `60000` (JSON) | Longer results are cut, with a note saying so |
|
|
108
|
+
|
|
109
|
+
`ScrapeUnblockerToolkit(include_plugins=False)` leaves out the two plugin tools.
|
|
110
|
+
|
|
111
|
+
Failed calls come back to the model as text it can act on (for example "the page itself answered 404, do not retry"), not as exceptions. Set `handle_tool_error=False` on a tool to raise instead.
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- [ScrapeUnblocker docs](https://docs.scrapeunblocker.com?utm_source=github&utm_medium=integration&utm_campaign=langchain)
|
|
116
|
+
- [Site plugins](https://docs.scrapeunblocker.com/plugins/overview?utm_source=github&utm_medium=integration&utm_campaign=langchain)
|
|
117
|
+
- [Python SDK](https://github.com/ScrapeUnblocker/scrapeunblocker-python)
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install -e ".[test]"
|
|
123
|
+
pytest # unit tests, no network
|
|
124
|
+
SCRAPEUNBLOCKER_API_KEY=... pytest tests/integration_tests # real API calls
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# langchain-scrapeunblocker
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/langchain-scrapeunblocker/)
|
|
4
|
+
[](https://github.com/ScrapeUnblocker/langchain-scrapeunblocker/actions/workflows/test.yml)
|
|
5
|
+
|
|
6
|
+
LangChain tools for [ScrapeUnblocker](https://scrapeunblocker.com?utm_source=github&utm_medium=integration&utm_campaign=langchain): give your agent live web data. Read any public page in a real browser as clean Markdown or structured JSON, search Google, and run 100+ ready-made site scrapers (Amazon, Walmart, eBay, Google Maps, YouTube, Zillow, Indeed and more), including pages behind Cloudflare, DataDome, PerimeterX and Akamai.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install -U langchain-scrapeunblocker
|
|
12
|
+
export SCRAPEUNBLOCKER_API_KEY="your-api-key"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Get a key at [app.scrapeunblocker.com](https://app.scrapeunblocker.com?utm_source=github&utm_medium=integration&utm_campaign=langchain) - a free account comes with 500 requests to start.
|
|
16
|
+
|
|
17
|
+
## Use with an agent
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from langchain.agents import create_agent
|
|
21
|
+
from langchain_scrapeunblocker import ScrapeUnblockerToolkit
|
|
22
|
+
|
|
23
|
+
agent = create_agent(
|
|
24
|
+
"anthropic:claude-sonnet-5-5",
|
|
25
|
+
tools=ScrapeUnblockerToolkit().get_tools(),
|
|
26
|
+
)
|
|
27
|
+
result = agent.invoke(
|
|
28
|
+
{"messages": [{"role": "user", "content": "What does the Echo Dot cost on amazon.com today?"}]}
|
|
29
|
+
)
|
|
30
|
+
print(result["messages"][-1].content)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
| Tool | Name the model sees | What it does | Billed |
|
|
36
|
+
|------|---------------------|--------------|--------|
|
|
37
|
+
| `ScrapeUnblockerMarkdown` | `scrapeunblocker_markdown` | A page as clean Markdown (main content, absolute links) | 1 request |
|
|
38
|
+
| `ScrapeUnblockerSearchAndRead` | `scrapeunblocker_search_and_read` | Google search plus the top 1-5 results read as Markdown, in one call | 1 + 1 per page |
|
|
39
|
+
| `ScrapeUnblockerGoogleSearch` | `scrapeunblocker_google_search` | Organic results, AI Overview, People also ask, related searches | 1 per result page |
|
|
40
|
+
| `ScrapeUnblockerParsedData` | `scrapeunblocker_parsed_data` | Structured JSON from a product, article or listing page | 1 request |
|
|
41
|
+
| `ScrapeUnblockerListPlugins` | `scrapeunblocker_list_plugins` | Search the catalog of 100+ site scrapers | free |
|
|
42
|
+
| `ScrapeUnblockerRunPlugin` | `scrapeunblocker_run_plugin` | Run a site scraper and get its JSON | per plugin |
|
|
43
|
+
|
|
44
|
+
Use them one by one:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from langchain_scrapeunblocker import ScrapeUnblockerMarkdown, ScrapeUnblockerRunPlugin
|
|
48
|
+
|
|
49
|
+
ScrapeUnblockerMarkdown().invoke({"url": "https://example.com"})
|
|
50
|
+
|
|
51
|
+
ScrapeUnblockerRunPlugin().invoke(
|
|
52
|
+
{"plugin": "youtube_transcript", "params": {"video": "dQw4w9WgXcQ"}}
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`ScrapeUnblockerGoogleSearch`, `ScrapeUnblockerParsedData` and `ScrapeUnblockerRunPlugin` return `content_and_artifact`: the model gets a compact text or JSON, and the full API response is on the `ToolMessage`'s `artifact` for your code. Called with a tool call, they return that message:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from langchain_scrapeunblocker import ScrapeUnblockerGoogleSearch
|
|
60
|
+
|
|
61
|
+
msg = ScrapeUnblockerGoogleSearch().invoke(
|
|
62
|
+
{"name": "scrapeunblocker_google_search", "args": {"query": "langchain"}, "id": "1", "type": "tool_call"}
|
|
63
|
+
)
|
|
64
|
+
msg.content # what the model reads
|
|
65
|
+
msg.artifact # the whole results page: ads, knowledge panel, local, shopping, videos...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Every tool takes an optional `proxy_country` (ISO code such as `"US"` or `"DE"`) to make the request from that country, and works with `ainvoke` too.
|
|
69
|
+
|
|
70
|
+
The plugin catalog comes from the installed [`scrapeunblocker`](https://pypi.org/project/scrapeunblocker/) SDK: `pip install -U scrapeunblocker` picks up new plugins without a new release of this package.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
| Argument | Default | |
|
|
75
|
+
|----------|---------|---|
|
|
76
|
+
| `api_key` | `SCRAPEUNBLOCKER_API_KEY` (or `SCRAPEUNBLOCKER_KEY`) | Your API key |
|
|
77
|
+
| `timeout` | `180` | Seconds per API call; protected pages can take a while |
|
|
78
|
+
| `max_retries` | `2` | Retries for 429 and 502-504 |
|
|
79
|
+
| `max_chars` | `100000` (Markdown), `60000` (JSON) | Longer results are cut, with a note saying so |
|
|
80
|
+
|
|
81
|
+
`ScrapeUnblockerToolkit(include_plugins=False)` leaves out the two plugin tools.
|
|
82
|
+
|
|
83
|
+
Failed calls come back to the model as text it can act on (for example "the page itself answered 404, do not retry"), not as exceptions. Set `handle_tool_error=False` on a tool to raise instead.
|
|
84
|
+
|
|
85
|
+
## Links
|
|
86
|
+
|
|
87
|
+
- [ScrapeUnblocker docs](https://docs.scrapeunblocker.com?utm_source=github&utm_medium=integration&utm_campaign=langchain)
|
|
88
|
+
- [Site plugins](https://docs.scrapeunblocker.com/plugins/overview?utm_source=github&utm_medium=integration&utm_campaign=langchain)
|
|
89
|
+
- [Python SDK](https://github.com/ScrapeUnblocker/scrapeunblocker-python)
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install -e ".[test]"
|
|
95
|
+
pytest # unit tests, no network
|
|
96
|
+
SCRAPEUNBLOCKER_API_KEY=... pytest tests/integration_tests # real API calls
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-scrapeunblocker"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "LangChain tools for ScrapeUnblocker: read any web page as Markdown or JSON, search Google, run 100+ site scrapers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["langchain", "scraping", "web-scraping", "agents", "llm", "serp", "markdown", "tools"]
|
|
13
|
+
authors = [{ name = "ScrapeUnblocker", email = "info@scrapeunblocker.com" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["langchain-core>=0.3.0,<2.0.0", "scrapeunblocker>=0.18.6"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest>=8", "pytest-asyncio>=0.23", "langchain-tests==1.1.9"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://scrapeunblocker.com?utm_source=pypi&utm_medium=integration&utm_campaign=langchain"
|
|
30
|
+
Documentation = "https://github.com/ScrapeUnblocker/langchain-scrapeunblocker#readme"
|
|
31
|
+
Source = "https://github.com/ScrapeUnblocker/langchain-scrapeunblocker"
|
|
32
|
+
Issues = "https://github.com/ScrapeUnblocker/langchain-scrapeunblocker/issues"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
path = "src/langchain_scrapeunblocker/version.py"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/langchain_scrapeunblocker"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests/unit_tests"]
|
|
42
|
+
asyncio_mode = "auto"
|
|
43
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""LangChain integration for ScrapeUnblocker: live web data for agents.
|
|
2
|
+
|
|
3
|
+
Read any page as Markdown or structured JSON in a real browser, search Google,
|
|
4
|
+
and run 100+ ready-made site scrapers. See https://docs.scrapeunblocker.com
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from langchain_scrapeunblocker.toolkit import ScrapeUnblockerToolkit
|
|
8
|
+
from langchain_scrapeunblocker.tools import (
|
|
9
|
+
ScrapeUnblockerGoogleSearch,
|
|
10
|
+
ScrapeUnblockerListPlugins,
|
|
11
|
+
ScrapeUnblockerMarkdown,
|
|
12
|
+
ScrapeUnblockerParsedData,
|
|
13
|
+
ScrapeUnblockerRunPlugin,
|
|
14
|
+
ScrapeUnblockerSearchAndRead,
|
|
15
|
+
)
|
|
16
|
+
from langchain_scrapeunblocker.version import __version__
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ScrapeUnblockerGoogleSearch",
|
|
20
|
+
"ScrapeUnblockerListPlugins",
|
|
21
|
+
"ScrapeUnblockerMarkdown",
|
|
22
|
+
"ScrapeUnblockerParsedData",
|
|
23
|
+
"ScrapeUnblockerRunPlugin",
|
|
24
|
+
"ScrapeUnblockerSearchAndRead",
|
|
25
|
+
"ScrapeUnblockerToolkit",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Shared plumbing for the ScrapeUnblocker tools: the API key, the SDK clients
|
|
2
|
+
and turning SDK errors into text an agent can act on."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from langchain_core.tools import BaseTool, ToolException
|
|
10
|
+
from pydantic import Field, PrivateAttr, SecretStr, model_validator
|
|
11
|
+
from scrapeunblocker import AsyncClient, Client
|
|
12
|
+
from scrapeunblocker.exceptions import (
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
BlockedError,
|
|
15
|
+
PaymentRequiredError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ScrapeTimeoutError,
|
|
18
|
+
ScrapeUnblockerError,
|
|
19
|
+
TargetNotFoundError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from langchain_scrapeunblocker.version import __version__
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.scrapeunblocker.com"
|
|
25
|
+
# Checked in this order; the SDK itself reads only the second.
|
|
26
|
+
API_KEY_ENV_VARS = ("SCRAPEUNBLOCKER_API_KEY", "SCRAPEUNBLOCKER_KEY")
|
|
27
|
+
SIGNUP_URL = (
|
|
28
|
+
"https://app.scrapeunblocker.com"
|
|
29
|
+
"?utm_source=langchain&utm_medium=integration&utm_campaign=langchain"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _key_from_env() -> Optional[str]:
|
|
34
|
+
for name in API_KEY_ENV_VARS:
|
|
35
|
+
value = os.environ.get(name)
|
|
36
|
+
if value:
|
|
37
|
+
return value
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cap_text(text: str, max_chars: int, hint: str) -> str:
|
|
42
|
+
"""Cut `text` to `max_chars`, saying so, so the agent knows it is partial."""
|
|
43
|
+
if len(text) <= max_chars:
|
|
44
|
+
return text
|
|
45
|
+
return f"{text[:max_chars]}\n\n[Cut at {max_chars} of {len(text)} characters. {hint}]"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def error_text(err: BaseException) -> str:
|
|
49
|
+
"""What the agent is told when a call fails, with what to do next."""
|
|
50
|
+
if isinstance(err, TargetNotFoundError):
|
|
51
|
+
return (
|
|
52
|
+
f"The page itself answered {err.origin_status} (not found or gone). "
|
|
53
|
+
"It was billed as one request; do not retry the same URL."
|
|
54
|
+
)
|
|
55
|
+
if isinstance(err, AuthenticationError):
|
|
56
|
+
return (
|
|
57
|
+
"ScrapeUnblocker rejected the API key, or the account has no active plan. "
|
|
58
|
+
f"Check the key or pick a plan at {SIGNUP_URL}"
|
|
59
|
+
)
|
|
60
|
+
if isinstance(err, PaymentRequiredError):
|
|
61
|
+
return f"The ScrapeUnblocker account is out of credits or over its limit: {err}"
|
|
62
|
+
if isinstance(err, BlockedError):
|
|
63
|
+
return f"The site blocked this request even through ScrapeUnblocker: {err}. Not billed."
|
|
64
|
+
if isinstance(err, RateLimitError):
|
|
65
|
+
return "Too many requests at once for this API key. Wait a moment and try again."
|
|
66
|
+
if isinstance(err, ScrapeTimeoutError):
|
|
67
|
+
return "The page took too long to load. Try again, or try a simpler URL."
|
|
68
|
+
return f"ScrapeUnblocker request failed: {err}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ScrapeUnblockerBaseTool(BaseTool):
|
|
72
|
+
"""Base class: owns the API key and the ScrapeUnblocker SDK clients."""
|
|
73
|
+
|
|
74
|
+
api_key: Optional[SecretStr] = Field(default=None)
|
|
75
|
+
"""ScrapeUnblocker API key. Read from ``SCRAPEUNBLOCKER_API_KEY`` (or
|
|
76
|
+
``SCRAPEUNBLOCKER_KEY``) when not passed."""
|
|
77
|
+
|
|
78
|
+
base_url: str = DEFAULT_BASE_URL
|
|
79
|
+
"""API host. Rarely needed."""
|
|
80
|
+
|
|
81
|
+
timeout: float = 180.0
|
|
82
|
+
"""Seconds to wait for one API call. Protected pages can take a while."""
|
|
83
|
+
|
|
84
|
+
max_retries: int = 2
|
|
85
|
+
"""Retries for transient failures (429, 502-504, network errors)."""
|
|
86
|
+
|
|
87
|
+
handle_tool_error: bool = True
|
|
88
|
+
"""Failed calls come back to the agent as text instead of raising."""
|
|
89
|
+
|
|
90
|
+
_client: Optional[Client] = PrivateAttr(default=None)
|
|
91
|
+
_aclient: Optional[AsyncClient] = PrivateAttr(default=None)
|
|
92
|
+
|
|
93
|
+
@model_validator(mode="before")
|
|
94
|
+
@classmethod
|
|
95
|
+
def _api_key_from_env(cls, values: Any) -> Any:
|
|
96
|
+
if isinstance(values, dict) and not values.get("api_key"):
|
|
97
|
+
key = _key_from_env()
|
|
98
|
+
if key:
|
|
99
|
+
values = {**values, "api_key": key}
|
|
100
|
+
return values
|
|
101
|
+
|
|
102
|
+
def _key(self) -> str:
|
|
103
|
+
if self.api_key is None or not self.api_key.get_secret_value():
|
|
104
|
+
raise ToolException(
|
|
105
|
+
"No ScrapeUnblocker API key. Pass api_key=... or set SCRAPEUNBLOCKER_API_KEY. "
|
|
106
|
+
f"Get a key at {SIGNUP_URL}"
|
|
107
|
+
)
|
|
108
|
+
return self.api_key.get_secret_value()
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _name_this_package(client: Any) -> None:
|
|
112
|
+
# The API counts calls per client library by User-Agent token.
|
|
113
|
+
try:
|
|
114
|
+
headers = client._http.headers
|
|
115
|
+
headers["User-Agent"] = f"{headers.get('User-Agent', '')} langchain-scrapeunblocker/{__version__}".strip()
|
|
116
|
+
except AttributeError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def client(self) -> Client:
|
|
121
|
+
if self._client is None:
|
|
122
|
+
self._client = Client(
|
|
123
|
+
self._key(), base_url=self.base_url, timeout=self.timeout, max_retries=self.max_retries
|
|
124
|
+
)
|
|
125
|
+
self._name_this_package(self._client)
|
|
126
|
+
return self._client
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def aclient(self) -> AsyncClient:
|
|
130
|
+
if self._aclient is None:
|
|
131
|
+
self._aclient = AsyncClient(
|
|
132
|
+
self._key(), base_url=self.base_url, timeout=self.timeout, max_retries=self.max_retries
|
|
133
|
+
)
|
|
134
|
+
self._name_this_package(self._aclient)
|
|
135
|
+
return self._aclient
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _fail(err: ScrapeUnblockerError) -> ToolException:
|
|
139
|
+
return ToolException(error_text(err))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The site plugin catalog, read from the installed ``scrapeunblocker`` SDK.
|
|
2
|
+
|
|
3
|
+
The SDK has one generated method per public plugin, with its parameters and a
|
|
4
|
+
docstring. Reading them here keeps this package in step with the SDK: a new
|
|
5
|
+
plugin needs an SDK upgrade, not a release of this package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from typing import Any, Dict, List, NamedTuple, Optional
|
|
13
|
+
|
|
14
|
+
from langchain_core.tools import ToolException
|
|
15
|
+
from scrapeunblocker import Client
|
|
16
|
+
|
|
17
|
+
# Plugins the SDK still has methods for but we do not offer.
|
|
18
|
+
_HIDDEN_PREFIXES = ("temu_",)
|
|
19
|
+
# Client methods that are not plugins.
|
|
20
|
+
_NOT_PLUGINS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"close",
|
|
23
|
+
"plugin",
|
|
24
|
+
"get_page_source",
|
|
25
|
+
"get_markdown",
|
|
26
|
+
"get_parsed",
|
|
27
|
+
"get_page_with_cookies",
|
|
28
|
+
"get_image",
|
|
29
|
+
"serp",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
_SUMMARY_MAX = 110
|
|
33
|
+
_LIST_MAX = 40
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Plugin(NamedTuple):
|
|
37
|
+
name: str
|
|
38
|
+
summary: str
|
|
39
|
+
doc: str
|
|
40
|
+
params: List[str]
|
|
41
|
+
signature: inspect.Signature
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _summary(doc: str) -> str:
|
|
45
|
+
first = " ".join(doc.split("\n\n", 1)[0].split())
|
|
46
|
+
return first if len(first) <= _SUMMARY_MAX else first[: _SUMMARY_MAX - 3].rstrip() + "..."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@lru_cache(maxsize=1)
|
|
50
|
+
def plugins() -> Dict[str, Plugin]:
|
|
51
|
+
found: Dict[str, Plugin] = {}
|
|
52
|
+
for name, member in inspect.getmembers(Client, inspect.isfunction):
|
|
53
|
+
if name.startswith("_") or name in _NOT_PLUGINS or name.startswith(_HIDDEN_PREFIXES):
|
|
54
|
+
continue
|
|
55
|
+
sig = inspect.signature(member)
|
|
56
|
+
params = [p for p in sig.parameters if p != "self" and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD]
|
|
57
|
+
doc = inspect.getdoc(member) or ""
|
|
58
|
+
found[name] = Plugin(name, _summary(doc), doc, params, sig)
|
|
59
|
+
return found
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _line(p: Plugin) -> str:
|
|
63
|
+
return f"- {p.name}({', '.join(p.params)}): {p.summary}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def describe(query: Optional[str] = None) -> str:
|
|
67
|
+
"""The catalog text for scrapeunblocker_list_plugins."""
|
|
68
|
+
catalog = plugins()
|
|
69
|
+
q = (query or "").strip().lower()
|
|
70
|
+
if q in catalog:
|
|
71
|
+
p = catalog[q]
|
|
72
|
+
return f"{p.name}({', '.join(p.params)})\n\n{p.doc}"
|
|
73
|
+
if not q:
|
|
74
|
+
# Names only: with summaries the whole list is too long for one tool result.
|
|
75
|
+
return (
|
|
76
|
+
f"{len(catalog)} plugins. Search with a word (e.g. 'amazon', 'reviews', 'jobs') to see what "
|
|
77
|
+
"they do, or pass an exact name for its parameters.\n\n" + ", ".join(catalog)
|
|
78
|
+
)
|
|
79
|
+
words = q.split()
|
|
80
|
+
matches = [p for p in catalog.values() if all(w in f"{p.name} {p.summary}".lower() for w in words)]
|
|
81
|
+
if not matches:
|
|
82
|
+
return f'No plugin matches "{query}". Call scrapeunblocker_list_plugins with no query to see all of them.'
|
|
83
|
+
shown = matches[:_LIST_MAX]
|
|
84
|
+
more = f" (showing {_LIST_MAX}; narrow the query)" if len(matches) > _LIST_MAX else ""
|
|
85
|
+
return f'{len(matches)} plugins match "{query}"{more}:\n\n' + "\n".join(_line(p) for p in shown)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def check_call(name: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
89
|
+
"""The keyword arguments for plugin `name`, or a ToolException the agent can fix."""
|
|
90
|
+
p = plugins().get(name)
|
|
91
|
+
if p is None:
|
|
92
|
+
raise ToolException(
|
|
93
|
+
f'There is no plugin "{name}". Find the right name with scrapeunblocker_list_plugins.'
|
|
94
|
+
)
|
|
95
|
+
if not isinstance(params, dict):
|
|
96
|
+
raise ToolException("params must be an object of parameter names to values.")
|
|
97
|
+
unknown = sorted(set(params) - set(p.params))
|
|
98
|
+
if unknown:
|
|
99
|
+
raise ToolException(
|
|
100
|
+
f"{name} has no parameter {', '.join(unknown)}. Its parameters: {', '.join(p.params) or 'none'}."
|
|
101
|
+
)
|
|
102
|
+
try:
|
|
103
|
+
p.signature.bind(None, **params)
|
|
104
|
+
except TypeError as err:
|
|
105
|
+
raise ToolException(f"Wrong parameters for {name}: {err}. Its parameters: {', '.join(p.params)}.") from err
|
|
106
|
+
return params
|