crewai-alterlab 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.
- crewai_alterlab-0.1.0/LICENSE +21 -0
- crewai_alterlab-0.1.0/PKG-INFO +151 -0
- crewai_alterlab-0.1.0/README.md +121 -0
- crewai_alterlab-0.1.0/crewai_alterlab/__init__.py +34 -0
- crewai_alterlab-0.1.0/crewai_alterlab/fallback.py +115 -0
- crewai_alterlab-0.1.0/crewai_alterlab/tools.py +330 -0
- crewai_alterlab-0.1.0/crewai_alterlab/utilities.py +73 -0
- crewai_alterlab-0.1.0/pyproject.toml +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 AlterLab
|
|
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,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crewai-alterlab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CrewAI integration for AlterLab web scraping API
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: crewai,alterlab,web-scraping,ai-agents,web-crawler,scraping-tool
|
|
8
|
+
Author: AlterLab
|
|
9
|
+
Author-email: dev@alterlab.io
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Dist: alterlab (>=2.4.0)
|
|
24
|
+
Requires-Dist: crewai[tools] (>=0.80.0)
|
|
25
|
+
Project-URL: Documentation, https://docs.alterlab.io/integrations/crewai
|
|
26
|
+
Project-URL: Homepage, https://alterlab.io
|
|
27
|
+
Project-URL: Repository, https://github.com/RapierCraftStudios/AlterLab
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# crewai-alterlab
|
|
31
|
+
|
|
32
|
+
CrewAI integration for the [AlterLab](https://alterlab.io) web scraping API.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install crewai-alterlab
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
Set your AlterLab API key:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export ALTERLAB_API_KEY="your-api-key"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Get your API key at [alterlab.io/dashboard](https://alterlab.io/dashboard).
|
|
49
|
+
|
|
50
|
+
## Scrape Tool
|
|
51
|
+
|
|
52
|
+
Scrape individual web pages with `AlterLabScrapeTool`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
56
|
+
|
|
57
|
+
tool = AlterLabScrapeTool()
|
|
58
|
+
result = tool.run("https://example.com")
|
|
59
|
+
print(result)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### With a CrewAI Agent
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from crewai import Agent, Task, Crew
|
|
66
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
67
|
+
|
|
68
|
+
researcher = Agent(
|
|
69
|
+
role="Web Researcher",
|
|
70
|
+
goal="Research topics by scraping web pages",
|
|
71
|
+
backstory="You are an expert web researcher.",
|
|
72
|
+
tools=[AlterLabScrapeTool()],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
task = Task(
|
|
76
|
+
description="Scrape https://example.com and summarise the content",
|
|
77
|
+
expected_output="A summary of the page content",
|
|
78
|
+
agent=researcher,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
crew = Crew(agents=[researcher], tasks=[task])
|
|
82
|
+
result = crew.kickoff()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Custom Parameters
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
tool = AlterLabScrapeTool(
|
|
89
|
+
scrape_params={
|
|
90
|
+
"formats": ["markdown"],
|
|
91
|
+
"mode": "js", # JavaScript rendering
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Crawl Tool
|
|
97
|
+
|
|
98
|
+
Crawl multiple pages from a website with `AlterLabCrawlTool`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from crewai_alterlab import AlterLabCrawlTool
|
|
102
|
+
|
|
103
|
+
tool = AlterLabCrawlTool()
|
|
104
|
+
result = tool.run(url="https://docs.example.com", max_pages=5)
|
|
105
|
+
print(result)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Research Agent with Both Tools
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from crewai import Agent
|
|
112
|
+
from crewai_alterlab import AlterLabScrapeTool, AlterLabCrawlTool
|
|
113
|
+
|
|
114
|
+
researcher = Agent(
|
|
115
|
+
role="Deep Researcher",
|
|
116
|
+
goal="Thoroughly research topics using web data",
|
|
117
|
+
backstory="You gather comprehensive information from the web.",
|
|
118
|
+
tools=[AlterLabScrapeTool(), AlterLabCrawlTool()],
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API Reference
|
|
123
|
+
|
|
124
|
+
See [docs.alterlab.io](https://docs.alterlab.io) for the full AlterLab API documentation.
|
|
125
|
+
|
|
126
|
+
## Search Tool
|
|
127
|
+
|
|
128
|
+
Run web searches with `AlterLabSearchTool` (numbered title, URL and snippet):
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from crewai_alterlab import AlterLabSearchTool
|
|
132
|
+
|
|
133
|
+
tool = AlterLabSearchTool(num_results=5, search_params={"engine": "google"})
|
|
134
|
+
print(tool.run(query="web scraping api"))
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Blocked-Fetch Fallback
|
|
138
|
+
|
|
139
|
+
`BlockedFetchFallbackTool` runs your own fetch first and retries through AlterLab only when
|
|
140
|
+
the result is a 403/429, a bot-protection challenge page, a CAPTCHA, or an empty JavaScript
|
|
141
|
+
shell. Normal pages, 404s and network errors are returned or raised untouched, so credits are
|
|
142
|
+
spent only on blocked fetches:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import requests
|
|
146
|
+
from crewai_alterlab import BlockedFetchFallbackTool
|
|
147
|
+
|
|
148
|
+
tool = BlockedFetchFallbackTool(fetch=lambda url: requests.get(url, timeout=20))
|
|
149
|
+
print(tool.run(url="https://example.com"))
|
|
150
|
+
```
|
|
151
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# crewai-alterlab
|
|
2
|
+
|
|
3
|
+
CrewAI integration for the [AlterLab](https://alterlab.io) web scraping API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install crewai-alterlab
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Set your AlterLab API key:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export ALTERLAB_API_KEY="your-api-key"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Get your API key at [alterlab.io/dashboard](https://alterlab.io/dashboard).
|
|
20
|
+
|
|
21
|
+
## Scrape Tool
|
|
22
|
+
|
|
23
|
+
Scrape individual web pages with `AlterLabScrapeTool`:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
27
|
+
|
|
28
|
+
tool = AlterLabScrapeTool()
|
|
29
|
+
result = tool.run("https://example.com")
|
|
30
|
+
print(result)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With a CrewAI Agent
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from crewai import Agent, Task, Crew
|
|
37
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
38
|
+
|
|
39
|
+
researcher = Agent(
|
|
40
|
+
role="Web Researcher",
|
|
41
|
+
goal="Research topics by scraping web pages",
|
|
42
|
+
backstory="You are an expert web researcher.",
|
|
43
|
+
tools=[AlterLabScrapeTool()],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
task = Task(
|
|
47
|
+
description="Scrape https://example.com and summarise the content",
|
|
48
|
+
expected_output="A summary of the page content",
|
|
49
|
+
agent=researcher,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
crew = Crew(agents=[researcher], tasks=[task])
|
|
53
|
+
result = crew.kickoff()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Custom Parameters
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
tool = AlterLabScrapeTool(
|
|
60
|
+
scrape_params={
|
|
61
|
+
"formats": ["markdown"],
|
|
62
|
+
"mode": "js", # JavaScript rendering
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Crawl Tool
|
|
68
|
+
|
|
69
|
+
Crawl multiple pages from a website with `AlterLabCrawlTool`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from crewai_alterlab import AlterLabCrawlTool
|
|
73
|
+
|
|
74
|
+
tool = AlterLabCrawlTool()
|
|
75
|
+
result = tool.run(url="https://docs.example.com", max_pages=5)
|
|
76
|
+
print(result)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Research Agent with Both Tools
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from crewai import Agent
|
|
83
|
+
from crewai_alterlab import AlterLabScrapeTool, AlterLabCrawlTool
|
|
84
|
+
|
|
85
|
+
researcher = Agent(
|
|
86
|
+
role="Deep Researcher",
|
|
87
|
+
goal="Thoroughly research topics using web data",
|
|
88
|
+
backstory="You gather comprehensive information from the web.",
|
|
89
|
+
tools=[AlterLabScrapeTool(), AlterLabCrawlTool()],
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API Reference
|
|
94
|
+
|
|
95
|
+
See [docs.alterlab.io](https://docs.alterlab.io) for the full AlterLab API documentation.
|
|
96
|
+
|
|
97
|
+
## Search Tool
|
|
98
|
+
|
|
99
|
+
Run web searches with `AlterLabSearchTool` (numbered title, URL and snippet):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from crewai_alterlab import AlterLabSearchTool
|
|
103
|
+
|
|
104
|
+
tool = AlterLabSearchTool(num_results=5, search_params={"engine": "google"})
|
|
105
|
+
print(tool.run(query="web scraping api"))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Blocked-Fetch Fallback
|
|
109
|
+
|
|
110
|
+
`BlockedFetchFallbackTool` runs your own fetch first and retries through AlterLab only when
|
|
111
|
+
the result is a 403/429, a bot-protection challenge page, a CAPTCHA, or an empty JavaScript
|
|
112
|
+
shell. Normal pages, 404s and network errors are returned or raised untouched, so credits are
|
|
113
|
+
spent only on blocked fetches:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import requests
|
|
117
|
+
from crewai_alterlab import BlockedFetchFallbackTool
|
|
118
|
+
|
|
119
|
+
tool = BlockedFetchFallbackTool(fetch=lambda url: requests.get(url, timeout=20))
|
|
120
|
+
print(tool.run(url="https://example.com"))
|
|
121
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""CrewAI integration for the AlterLab web scraping API.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- ``AlterLabScrapeTool``: Tool for CrewAI agents to scrape individual web pages.
|
|
5
|
+
- ``AlterLabCrawlTool``: Tool for CrewAI agents to crawl websites.
|
|
6
|
+
- ``AlterLabSearchTool``: Tool for CrewAI agents to run web searches.
|
|
7
|
+
|
|
8
|
+
Install::
|
|
9
|
+
|
|
10
|
+
pip install crewai-alterlab
|
|
11
|
+
|
|
12
|
+
Quick start::
|
|
13
|
+
|
|
14
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
15
|
+
|
|
16
|
+
tool = AlterLabScrapeTool()
|
|
17
|
+
result = tool.run("https://example.com")
|
|
18
|
+
print(result)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from crewai_alterlab.fallback import AlterLabFallbackTool as BlockedFetchFallbackTool
|
|
22
|
+
from crewai_alterlab.tools import (
|
|
23
|
+
AlterLabCrawlTool,
|
|
24
|
+
AlterLabScrapeTool,
|
|
25
|
+
AlterLabSearchTool,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
__all__ = [
|
|
30
|
+
"BlockedFetchFallbackTool",
|
|
31
|
+
"AlterLabScrapeTool",
|
|
32
|
+
"AlterLabCrawlTool",
|
|
33
|
+
"AlterLabSearchTool",
|
|
34
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Blocked-fetch fallback tool for CrewAI agents.
|
|
2
|
+
|
|
3
|
+
``AlterLabFallbackTool`` tries the caller's own fetch first and retries the URL through
|
|
4
|
+
AlterLab only when the response carries a block signal: an HTTP 403/429, a bot-protection
|
|
5
|
+
challenge page, a CAPTCHA, or an empty JavaScript shell. Any other outcome (a normal page,
|
|
6
|
+
a 404, a network error) is returned or raised untouched, so AlterLab credits are spent only
|
|
7
|
+
when the plain fetch was actually blocked.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
from crewai_alterlab import BlockedFetchFallbackTool
|
|
13
|
+
|
|
14
|
+
tool = BlockedFetchFallbackTool(fetch=lambda url: requests.get(url, timeout=20))
|
|
15
|
+
tool.run(url="https://example.com")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from typing import Any, Callable, Optional, Type
|
|
21
|
+
|
|
22
|
+
from crewai.tools import BaseTool
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
BLOCK_STATUS_CODES = frozenset({403, 429})
|
|
28
|
+
|
|
29
|
+
_CHALLENGE_MARKERS = re.compile(
|
|
30
|
+
r"just a moment|checking your browser|verify you are (?:a )?human|attention required"
|
|
31
|
+
r"|access denied|captcha|are you a robot|unusual traffic|enable javascript and cookies"
|
|
32
|
+
r"|px-captcha|challenge-platform|_incapsula_|datadome",
|
|
33
|
+
re.IGNORECASE,
|
|
34
|
+
)
|
|
35
|
+
_SCRIPT_OR_STYLE = re.compile(
|
|
36
|
+
r"<(script|style|noscript)\b.*?</\1>", re.IGNORECASE | re.DOTALL
|
|
37
|
+
)
|
|
38
|
+
_TAGS = re.compile(r"<[^>]+>")
|
|
39
|
+
_JS_REQUIRED = re.compile(r"enable javascript|requires javascript", re.IGNORECASE)
|
|
40
|
+
_EMPTY_SHELL_MAX_CHARS = 80
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _visible_text(html: str) -> str:
|
|
44
|
+
without_blocks = _SCRIPT_OR_STYLE.sub(" ", html)
|
|
45
|
+
return " ".join(_TAGS.sub(" ", without_blocks).split())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def is_blocked(status_code: Optional[int], body: str) -> bool:
|
|
49
|
+
"""Return True when a plain fetch result looks blocked rather than served."""
|
|
50
|
+
if status_code in BLOCK_STATUS_CODES:
|
|
51
|
+
return True
|
|
52
|
+
if status_code is not None and not 200 <= status_code < 300:
|
|
53
|
+
return False
|
|
54
|
+
if _CHALLENGE_MARKERS.search(body):
|
|
55
|
+
return True
|
|
56
|
+
looks_like_html = (
|
|
57
|
+
"<html" in body.lower() or "<body" in body.lower() or "<div" in body.lower()
|
|
58
|
+
)
|
|
59
|
+
if looks_like_html:
|
|
60
|
+
if len(_visible_text(body)) <= _EMPTY_SHELL_MAX_CHARS:
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
if _JS_REQUIRED.search(body):
|
|
64
|
+
return len(body.strip()) <= _EMPTY_SHELL_MAX_CHARS * 4
|
|
65
|
+
return not body.strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _unpack(result: Any) -> "tuple[Optional[int], str]":
|
|
69
|
+
"""Accept a str, a ``(status, text)`` tuple, or a requests/httpx-style response."""
|
|
70
|
+
if isinstance(result, str):
|
|
71
|
+
return None, result
|
|
72
|
+
if isinstance(result, tuple) and len(result) == 2:
|
|
73
|
+
status, text = result
|
|
74
|
+
return status, text or ""
|
|
75
|
+
status = getattr(result, "status_code", None)
|
|
76
|
+
text = getattr(result, "text", "")
|
|
77
|
+
return status, text if isinstance(text, str) else str(text or "")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _FallbackInput(BaseModel):
|
|
81
|
+
url: str = Field(description="The URL of the web page to read.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AlterLabFallbackTool(BaseTool):
|
|
85
|
+
"""Fetch a URL with the caller's fetch; retry via AlterLab only if it was blocked."""
|
|
86
|
+
|
|
87
|
+
name: str = "AlterLab Blocked Fetch Fallback"
|
|
88
|
+
description: str = (
|
|
89
|
+
"Read a web page. Tries a normal fetch first and automatically retries through "
|
|
90
|
+
"AlterLab when that fetch returns 403/429, a bot-protection challenge page, a "
|
|
91
|
+
"CAPTCHA, or an empty JavaScript shell. Input is a URL; returns the page content."
|
|
92
|
+
)
|
|
93
|
+
args_schema: Type[BaseModel] = _FallbackInput
|
|
94
|
+
|
|
95
|
+
fetch: Callable[[str], Any]
|
|
96
|
+
"""Caller's fetch: returns a str, a ``(status_code, text)`` tuple, or a response object."""
|
|
97
|
+
|
|
98
|
+
fallback: Optional[BaseTool] = None
|
|
99
|
+
"""Tool used on a block signal. Defaults to ``AlterLabScrapeTool()``."""
|
|
100
|
+
|
|
101
|
+
def _fallback_tool(self) -> BaseTool:
|
|
102
|
+
if self.fallback is None:
|
|
103
|
+
from crewai_alterlab.tools import AlterLabScrapeTool
|
|
104
|
+
|
|
105
|
+
self.fallback = AlterLabScrapeTool()
|
|
106
|
+
return self.fallback
|
|
107
|
+
|
|
108
|
+
def _run(self, url: str) -> str:
|
|
109
|
+
status, body = _unpack(self.fetch(url))
|
|
110
|
+
if not is_blocked(status, body):
|
|
111
|
+
return body
|
|
112
|
+
logger.info(
|
|
113
|
+
"Blocked fetch for %s (status=%s); retrying via AlterLab", url, status
|
|
114
|
+
)
|
|
115
|
+
return self._fallback_tool().run(url=url)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""AlterLab tools for CrewAI agents.
|
|
2
|
+
|
|
3
|
+
Provides ``AlterLabScrapeTool`` and ``AlterLabCrawlTool`` — CrewAI-compatible
|
|
4
|
+
tools that allow agents to scrape and crawl web pages via the AlterLab API.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
9
|
+
|
|
10
|
+
tool = AlterLabScrapeTool()
|
|
11
|
+
# Use with a CrewAI agent
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Dict, Optional, Type
|
|
16
|
+
|
|
17
|
+
from crewai.tools import BaseTool
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from crewai_alterlab.utilities import (
|
|
21
|
+
extract_page_content,
|
|
22
|
+
get_api_key,
|
|
23
|
+
get_api_url,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _ScrapeInput(BaseModel):
|
|
30
|
+
"""Input schema for the AlterLab scrape tool."""
|
|
31
|
+
|
|
32
|
+
url: str = Field(description="The URL of the web page to scrape.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AlterLabScrapeTool(BaseTool):
|
|
36
|
+
"""CrewAI tool that scrapes a web page using the AlterLab API.
|
|
37
|
+
|
|
38
|
+
Returns the page content as a string (markdown preferred). Designed for
|
|
39
|
+
use with CrewAI agents that need to fetch and read web page content.
|
|
40
|
+
|
|
41
|
+
Setup:
|
|
42
|
+
Install the package and set the ``ALTERLAB_API_KEY`` environment
|
|
43
|
+
variable::
|
|
44
|
+
|
|
45
|
+
pip install crewai-alterlab
|
|
46
|
+
export ALTERLAB_API_KEY="your-api-key"
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
.. code-block:: python
|
|
50
|
+
|
|
51
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
52
|
+
|
|
53
|
+
tool = AlterLabScrapeTool()
|
|
54
|
+
result = tool.run("https://example.com")
|
|
55
|
+
print(result)
|
|
56
|
+
|
|
57
|
+
With a CrewAI agent:
|
|
58
|
+
.. code-block:: python
|
|
59
|
+
|
|
60
|
+
from crewai import Agent
|
|
61
|
+
from crewai_alterlab import AlterLabScrapeTool
|
|
62
|
+
|
|
63
|
+
researcher = Agent(
|
|
64
|
+
role="Web Researcher",
|
|
65
|
+
goal="Research topics by scraping web pages",
|
|
66
|
+
tools=[AlterLabScrapeTool()],
|
|
67
|
+
)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
name: str = "AlterLab Scrape"
|
|
71
|
+
description: str = (
|
|
72
|
+
"Scrape a web page and return its content as text. "
|
|
73
|
+
"Input is a URL string. Returns the page content as markdown text. "
|
|
74
|
+
"Useful for reading web pages, articles, documentation, or any "
|
|
75
|
+
"publicly accessible URL."
|
|
76
|
+
)
|
|
77
|
+
args_schema: Type[BaseModel] = _ScrapeInput
|
|
78
|
+
|
|
79
|
+
api_key: Optional[str] = None
|
|
80
|
+
"""AlterLab API key. Defaults to ALTERLAB_API_KEY env var."""
|
|
81
|
+
|
|
82
|
+
api_url: Optional[str] = None
|
|
83
|
+
"""AlterLab API base URL. Defaults to https://api.alterlab.io."""
|
|
84
|
+
|
|
85
|
+
scrape_params: Dict[str, Any] = Field(default_factory=dict)
|
|
86
|
+
"""Additional parameters passed to the AlterLab scrape API."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
89
|
+
"""Initialise AlterLabScrapeTool.
|
|
90
|
+
|
|
91
|
+
Validates the API key is available at construction time.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ImportError: If the ``alterlab`` package is not installed.
|
|
95
|
+
ValueError: If no API key can be resolved.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import alterlab # noqa: F401
|
|
99
|
+
except ImportError:
|
|
100
|
+
raise ImportError(
|
|
101
|
+
"Could not import alterlab python package. "
|
|
102
|
+
"Please install it with `pip install alterlab`."
|
|
103
|
+
)
|
|
104
|
+
super().__init__(**kwargs)
|
|
105
|
+
# Validate key is resolvable (raises ValueError if not)
|
|
106
|
+
get_api_key(self.api_key)
|
|
107
|
+
|
|
108
|
+
def _run(self, url: str) -> str:
|
|
109
|
+
"""Scrape a URL and return its content.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
url: The URL to scrape.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Page content as a string, or an error message.
|
|
116
|
+
"""
|
|
117
|
+
from alterlab import AlterLabSync
|
|
118
|
+
|
|
119
|
+
resolved_key = get_api_key(self.api_key)
|
|
120
|
+
resolved_url = get_api_url(self.api_url)
|
|
121
|
+
|
|
122
|
+
client = AlterLabSync(api_key=resolved_key, base_url=resolved_url)
|
|
123
|
+
try:
|
|
124
|
+
result = client.scrape(url, **self.scrape_params)
|
|
125
|
+
content = extract_page_content(result)
|
|
126
|
+
if not content:
|
|
127
|
+
return f"No content could be extracted from {url}"
|
|
128
|
+
return content
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.warning("AlterLab scrape failed for %s: %s", url, e)
|
|
131
|
+
return f"Error scraping {url}: {e}"
|
|
132
|
+
finally:
|
|
133
|
+
client.close()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class _CrawlInput(BaseModel):
|
|
137
|
+
"""Input schema for the AlterLab crawl tool."""
|
|
138
|
+
|
|
139
|
+
url: str = Field(
|
|
140
|
+
description="The starting URL for the crawl."
|
|
141
|
+
)
|
|
142
|
+
max_pages: int = Field(
|
|
143
|
+
default=10,
|
|
144
|
+
description="Maximum number of pages to crawl.",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class AlterLabCrawlTool(BaseTool):
|
|
149
|
+
"""CrewAI tool that crawls a website using the AlterLab API.
|
|
150
|
+
|
|
151
|
+
Crawls multiple pages starting from a URL and returns their combined
|
|
152
|
+
content. Useful for gathering comprehensive information from a website.
|
|
153
|
+
|
|
154
|
+
Setup:
|
|
155
|
+
Install the package and set the ``ALTERLAB_API_KEY`` environment
|
|
156
|
+
variable::
|
|
157
|
+
|
|
158
|
+
pip install crewai-alterlab
|
|
159
|
+
export ALTERLAB_API_KEY="your-api-key"
|
|
160
|
+
|
|
161
|
+
Usage:
|
|
162
|
+
.. code-block:: python
|
|
163
|
+
|
|
164
|
+
from crewai_alterlab import AlterLabCrawlTool
|
|
165
|
+
|
|
166
|
+
tool = AlterLabCrawlTool()
|
|
167
|
+
result = tool.run(url="https://docs.example.com", max_pages=5)
|
|
168
|
+
print(result)
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
name: str = "AlterLab Crawl"
|
|
172
|
+
description: str = (
|
|
173
|
+
"Crawl a website starting from a URL and return content from "
|
|
174
|
+
"multiple pages. Input requires a URL and optional max_pages "
|
|
175
|
+
"(default 10). Useful for gathering comprehensive information "
|
|
176
|
+
"from documentation sites, blogs, or multi-page resources."
|
|
177
|
+
)
|
|
178
|
+
args_schema: Type[BaseModel] = _CrawlInput
|
|
179
|
+
|
|
180
|
+
api_key: Optional[str] = None
|
|
181
|
+
"""AlterLab API key. Defaults to ALTERLAB_API_KEY env var."""
|
|
182
|
+
|
|
183
|
+
api_url: Optional[str] = None
|
|
184
|
+
"""AlterLab API base URL. Defaults to https://api.alterlab.io."""
|
|
185
|
+
|
|
186
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
187
|
+
"""Initialise AlterLabCrawlTool.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ImportError: If the ``alterlab`` package is not installed.
|
|
191
|
+
ValueError: If no API key can be resolved.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
import alterlab # noqa: F401
|
|
195
|
+
except ImportError:
|
|
196
|
+
raise ImportError(
|
|
197
|
+
"Could not import alterlab python package. "
|
|
198
|
+
"Please install it with `pip install alterlab`."
|
|
199
|
+
)
|
|
200
|
+
super().__init__(**kwargs)
|
|
201
|
+
get_api_key(self.api_key)
|
|
202
|
+
|
|
203
|
+
def _run(self, url: str, max_pages: int = 10) -> str:
|
|
204
|
+
"""Crawl a website and return combined content.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
url: The starting URL for the crawl.
|
|
208
|
+
max_pages: Maximum number of pages to crawl.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Combined page content as a string, or an error message.
|
|
212
|
+
"""
|
|
213
|
+
from alterlab import AlterLabSync
|
|
214
|
+
|
|
215
|
+
resolved_key = get_api_key(self.api_key)
|
|
216
|
+
resolved_url = get_api_url(self.api_url)
|
|
217
|
+
|
|
218
|
+
client = AlterLabSync(api_key=resolved_key, base_url=resolved_url)
|
|
219
|
+
try:
|
|
220
|
+
result = client.crawl(url, max_pages=max_pages)
|
|
221
|
+
|
|
222
|
+
pages = result.get("pages", [])
|
|
223
|
+
if not pages:
|
|
224
|
+
return f"No pages could be crawled from {url}"
|
|
225
|
+
|
|
226
|
+
parts = []
|
|
227
|
+
for page in pages:
|
|
228
|
+
page_url = page.get("url", "unknown")
|
|
229
|
+
content = extract_page_content(page)
|
|
230
|
+
if content:
|
|
231
|
+
parts.append(f"## {page_url}\n\n{content}")
|
|
232
|
+
|
|
233
|
+
if not parts:
|
|
234
|
+
return f"No content could be extracted from crawled pages at {url}"
|
|
235
|
+
|
|
236
|
+
return "\n\n---\n\n".join(parts)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.warning("AlterLab crawl failed for %s: %s", url, e)
|
|
239
|
+
return f"Error crawling {url}: {e}"
|
|
240
|
+
finally:
|
|
241
|
+
client.close()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _format_search_results(result: Dict[str, Any], query: str) -> str:
|
|
245
|
+
"""Format a SERP response as numbered ``title / url / snippet`` text."""
|
|
246
|
+
items = result.get("results") or []
|
|
247
|
+
if not items:
|
|
248
|
+
return f"No search results found for {query!r}"
|
|
249
|
+
lines = []
|
|
250
|
+
for i, item in enumerate(items, start=1):
|
|
251
|
+
pos = item.get("position") or i
|
|
252
|
+
lines.append(f"{pos}. {item.get('title', '')}\n {item.get('url', '')}")
|
|
253
|
+
if item.get("snippet"):
|
|
254
|
+
lines.append(f" {item['snippet']}")
|
|
255
|
+
return "\n".join(lines)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class _SearchInput(BaseModel):
|
|
259
|
+
"""Input schema for the AlterLab search tool."""
|
|
260
|
+
|
|
261
|
+
query: str = Field(description="The search query (max 500 characters).")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class AlterLabSearchTool(BaseTool):
|
|
265
|
+
"""CrewAI tool that runs a web search using the AlterLab Search API.
|
|
266
|
+
|
|
267
|
+
Returns numbered results (title, URL, snippet) as a string.
|
|
268
|
+
|
|
269
|
+
Usage:
|
|
270
|
+
.. code-block:: python
|
|
271
|
+
|
|
272
|
+
from crewai_alterlab import AlterLabSearchTool
|
|
273
|
+
|
|
274
|
+
tool = AlterLabSearchTool(num_results=5)
|
|
275
|
+
print(tool.run(query="web scraping api"))
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
name: str = "AlterLab Search"
|
|
279
|
+
description: str = (
|
|
280
|
+
"Search the web and return a numbered list of results with title, "
|
|
281
|
+
"URL and snippet. Input is a search query string."
|
|
282
|
+
)
|
|
283
|
+
args_schema: Type[BaseModel] = _SearchInput
|
|
284
|
+
|
|
285
|
+
api_key: Optional[str] = None
|
|
286
|
+
"""AlterLab API key. Defaults to ALTERLAB_API_KEY env var."""
|
|
287
|
+
|
|
288
|
+
api_url: Optional[str] = None
|
|
289
|
+
"""AlterLab API base URL. Defaults to https://api.alterlab.io."""
|
|
290
|
+
|
|
291
|
+
num_results: int = 10
|
|
292
|
+
"""Number of results to return (1-30)."""
|
|
293
|
+
|
|
294
|
+
search_params: Dict[str, Any] = Field(default_factory=dict)
|
|
295
|
+
"""Additional parameters passed to the AlterLab search API (e.g. engine, domain)."""
|
|
296
|
+
|
|
297
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
298
|
+
"""Initialise AlterLabSearchTool.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
ImportError: If the ``alterlab`` package is not installed.
|
|
302
|
+
ValueError: If no API key can be resolved.
|
|
303
|
+
"""
|
|
304
|
+
try:
|
|
305
|
+
import alterlab # noqa: F401
|
|
306
|
+
except ImportError:
|
|
307
|
+
raise ImportError(
|
|
308
|
+
"Could not import alterlab python package. "
|
|
309
|
+
"Please install it with `pip install alterlab`."
|
|
310
|
+
)
|
|
311
|
+
super().__init__(**kwargs)
|
|
312
|
+
get_api_key(self.api_key)
|
|
313
|
+
|
|
314
|
+
def _run(self, query: str) -> str:
|
|
315
|
+
"""Search the web and return formatted results, or an error message."""
|
|
316
|
+
from alterlab import AlterLabSync
|
|
317
|
+
|
|
318
|
+
client = AlterLabSync(
|
|
319
|
+
api_key=get_api_key(self.api_key), base_url=get_api_url(self.api_url)
|
|
320
|
+
)
|
|
321
|
+
try:
|
|
322
|
+
result = client.search(
|
|
323
|
+
query, num_results=self.num_results, **self.search_params
|
|
324
|
+
)
|
|
325
|
+
return _format_search_results(result, query)
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.warning("AlterLab search failed for %r: %s", query, e)
|
|
328
|
+
return f"Error searching {query!r}: {e}"
|
|
329
|
+
finally:
|
|
330
|
+
client.close()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Shared AlterLab API wrapper used by CrewAI tools.
|
|
2
|
+
|
|
3
|
+
Centralises client construction and response-to-content extraction so that
|
|
4
|
+
tool classes stay thin.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_api_key(api_key: Optional[str] = None) -> str:
|
|
12
|
+
"""Resolve the AlterLab API key.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
api_key: Explicit key. Falls back to the ``ALTERLAB_API_KEY``
|
|
16
|
+
environment variable.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The resolved API key.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If no key can be resolved.
|
|
23
|
+
"""
|
|
24
|
+
key = api_key or os.getenv("ALTERLAB_API_KEY", "")
|
|
25
|
+
if not key:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"AlterLab API key is required. Pass api_key or set the "
|
|
28
|
+
"ALTERLAB_API_KEY environment variable."
|
|
29
|
+
)
|
|
30
|
+
return key
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_api_url(api_url: Optional[str] = None) -> str:
|
|
34
|
+
"""Resolve the AlterLab API base URL.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
api_url: Explicit URL. Falls back to ``ALTERLAB_API_URL`` env var
|
|
38
|
+
or ``https://api.alterlab.io``.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The resolved base URL.
|
|
42
|
+
"""
|
|
43
|
+
return api_url or os.getenv("ALTERLAB_API_URL") or "https://api.alterlab.io"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def extract_page_content(result: Dict[str, Any]) -> str:
|
|
47
|
+
"""Extract page content from an AlterLab API response.
|
|
48
|
+
|
|
49
|
+
Priority order: markdown > text > html > raw content string.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
result: AlterLab API response dictionary.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Extracted content string, or empty string if no content found.
|
|
56
|
+
"""
|
|
57
|
+
content = result.get("content")
|
|
58
|
+
|
|
59
|
+
# Multi-format response (dict with format keys)
|
|
60
|
+
if isinstance(content, dict):
|
|
61
|
+
if content.get("markdown"):
|
|
62
|
+
return content["markdown"]
|
|
63
|
+
if content.get("text"):
|
|
64
|
+
return content["text"]
|
|
65
|
+
if content.get("html"):
|
|
66
|
+
return content["html"]
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
# Legacy single-format response (string)
|
|
70
|
+
if isinstance(content, str):
|
|
71
|
+
return content
|
|
72
|
+
|
|
73
|
+
return ""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "crewai-alterlab"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CrewAI integration for AlterLab web scraping API"
|
|
9
|
+
authors = ["AlterLab <dev@alterlab.io>"]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
repository = "https://github.com/RapierCraftStudios/AlterLab"
|
|
13
|
+
homepage = "https://alterlab.io"
|
|
14
|
+
documentation = "https://docs.alterlab.io/integrations/crewai"
|
|
15
|
+
keywords = ["crewai", "alterlab", "web-scraping", "ai-agents", "web-crawler", "scraping-tool"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
27
|
+
]
|
|
28
|
+
packages = [{ include = "crewai_alterlab" }]
|
|
29
|
+
|
|
30
|
+
[tool.poetry.dependencies]
|
|
31
|
+
python = ">=3.10,<4.0"
|
|
32
|
+
crewai = {version = ">=0.80.0", extras = ["tools"]}
|
|
33
|
+
alterlab = ">=2.4.0"
|
|
34
|
+
|
|
35
|
+
[tool.poetry.group.dev.dependencies]
|
|
36
|
+
pytest = ">=7.4.0"
|
|
37
|
+
|
|
38
|
+
[tool.black]
|
|
39
|
+
line-length = 88
|
|
40
|
+
target-version = ["py310"]
|
|
41
|
+
|
|
42
|
+
[tool.isort]
|
|
43
|
+
profile = "black"
|
|
44
|
+
line_length = 88
|