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.
@@ -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