ellements 0.2.0__py3-none-any.whl
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.
- ellements/__init__.py +57 -0
- ellements/agents/__init__.py +45 -0
- ellements/agents/backend.py +100 -0
- ellements/agents/builder.py +303 -0
- ellements/agents/claude_backend.py +200 -0
- ellements/agents/controller.py +237 -0
- ellements/agents/openai_backend.py +187 -0
- ellements/agents/py.typed +0 -0
- ellements/agents/runner.py +358 -0
- ellements/agents/tools.py +30 -0
- ellements/benchmarking/__init__.py +52 -0
- ellements/benchmarking/harness.py +342 -0
- ellements/benchmarking/py.typed +0 -0
- ellements/benchmarking/results.py +173 -0
- ellements/cli/__init__.py +80 -0
- ellements/cli/adapters.py +73 -0
- ellements/cli/agent_tui.py +1178 -0
- ellements/cli/components.py +411 -0
- ellements/cli/printer.py +229 -0
- ellements/cli/py.typed +0 -0
- ellements/core/__init__.py +112 -0
- ellements/core/async_utils.py +42 -0
- ellements/core/budgeting/__init__.py +43 -0
- ellements/core/budgeting/client.py +276 -0
- ellements/core/budgeting/protocol.py +51 -0
- ellements/core/budgeting/trackers.py +177 -0
- ellements/core/caching/__init__.py +51 -0
- ellements/core/caching/cache.py +73 -0
- ellements/core/caching/client.py +300 -0
- ellements/core/caching/disk.py +128 -0
- ellements/core/caching/keys.py +97 -0
- ellements/core/caching/memory.py +104 -0
- ellements/core/chunking.py +262 -0
- ellements/core/config.py +180 -0
- ellements/core/exceptions.py +145 -0
- ellements/core/llm/__init__.py +46 -0
- ellements/core/llm/client.py +1124 -0
- ellements/core/llm/images.py +226 -0
- ellements/core/llm/messages.py +202 -0
- ellements/core/llm/model_params.py +66 -0
- ellements/core/llm/protocol.py +146 -0
- ellements/core/llm/requests.py +100 -0
- ellements/core/llm/structured.py +91 -0
- ellements/core/llm/wrapper.py +79 -0
- ellements/core/observability/__init__.py +39 -0
- ellements/core/observability/events.py +169 -0
- ellements/core/observability/jsonl_logger.py +244 -0
- ellements/core/observability/markdown_formatter.py +197 -0
- ellements/core/observability/observer.py +56 -0
- ellements/core/prompting/__init__.py +14 -0
- ellements/core/prompting/context.py +185 -0
- ellements/core/prompting/guideline.py +133 -0
- ellements/core/prompting/persona.py +267 -0
- ellements/core/prompting/sources.py +92 -0
- ellements/core/py.typed +0 -0
- ellements/core/rate_limit/__init__.py +44 -0
- ellements/core/rate_limit/bucket.py +85 -0
- ellements/core/rate_limit/client.py +216 -0
- ellements/core/rate_limit/protocol.py +27 -0
- ellements/core/templating.py +126 -0
- ellements/core/tools/__init__.py +51 -0
- ellements/core/tools/dialects.py +136 -0
- ellements/core/tools/executor.py +48 -0
- ellements/core/tools/protocol.py +36 -0
- ellements/core/tools/records.py +28 -0
- ellements/core/tools/registry.py +205 -0
- ellements/core/tools/schemas.py +80 -0
- ellements/core/tools/simple.py +119 -0
- ellements/core/tools/spec.py +33 -0
- ellements/domain_specific/__init__.py +7 -0
- ellements/domain_specific/finance/__init__.py +188 -0
- ellements/domain_specific/finance/calculations.py +837 -0
- ellements/domain_specific/finance/charts.py +426 -0
- ellements/domain_specific/finance/portfolio.py +129 -0
- ellements/domain_specific/finance/quant_analysis.py +279 -0
- ellements/domain_specific/finance/risk.py +362 -0
- ellements/domain_specific/finance/technical_indicators.py +241 -0
- ellements/domain_specific/finance/tools.py +483 -0
- ellements/domain_specific/finance/valuation.py +312 -0
- ellements/domain_specific/finance/yahoo_finance.py +1523 -0
- ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
- ellements/domain_specific/py.typed +0 -0
- ellements/execution/__init__.py +56 -0
- ellements/execution/callbacks.py +149 -0
- ellements/execution/catalog.py +70 -0
- ellements/execution/collaborative.py +191 -0
- ellements/execution/config.py +135 -0
- ellements/execution/py.typed +0 -0
- ellements/execution/reflection.py +156 -0
- ellements/execution/self_consistency.py +189 -0
- ellements/execution/single_call.py +48 -0
- ellements/execution/strategies.py +237 -0
- ellements/execution/tree_of_thought.py +541 -0
- ellements/fslm/__init__.py +108 -0
- ellements/fslm/builtins.py +103 -0
- ellements/fslm/cli.py +199 -0
- ellements/fslm/context.py +50 -0
- ellements/fslm/definition.py +163 -0
- ellements/fslm/det.py +173 -0
- ellements/fslm/dsl.py +458 -0
- ellements/fslm/errors.py +38 -0
- ellements/fslm/evaluators.py +141 -0
- ellements/fslm/kernel.py +603 -0
- ellements/fslm/loading.py +123 -0
- ellements/fslm/models.py +623 -0
- ellements/fslm/nl.py +87 -0
- ellements/fslm/observers.py +107 -0
- ellements/fslm/persistence.py +72 -0
- ellements/fslm/py.typed +0 -0
- ellements/fslm/rendering.py +551 -0
- ellements/fslm/visualization.py +25 -0
- ellements/reporting/__init__.py +12 -0
- ellements/reporting/charts.py +364 -0
- ellements/reporting/html_generation.py +132 -0
- ellements/reporting/py.typed +0 -0
- ellements/reporting/visualization.py +73 -0
- ellements/standard_tools/__init__.py +22 -0
- ellements/standard_tools/py.typed +0 -0
- ellements/standard_tools/terminal.py +205 -0
- ellements/standard_tools/web/__init__.py +37 -0
- ellements/standard_tools/web/browser_viewer.py +214 -0
- ellements/standard_tools/web/crawler.py +454 -0
- ellements/standard_tools/web/search.py +399 -0
- ellements/standard_tools/web/youtube.py +1297 -0
- ellements-0.2.0.dist-info/METADATA +368 -0
- ellements-0.2.0.dist-info/RECORD +130 -0
- ellements-0.2.0.dist-info/WHEEL +5 -0
- ellements-0.2.0.dist-info/entry_points.txt +2 -0
- ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
- ellements-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"""AI-optimized web crawling using Crawl4AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urljoin
|
|
8
|
+
|
|
9
|
+
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
|
10
|
+
from crawl4ai.extraction_strategy import CosineStrategy, LLMExtractionStrategy
|
|
11
|
+
from ellements.core import ToolRegistry
|
|
12
|
+
from ellements.core.exceptions import LLMError
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CrawlResult(BaseModel):
|
|
17
|
+
"""Represents the result of crawling a single URL."""
|
|
18
|
+
|
|
19
|
+
url: str = Field(description="The URL that was crawled")
|
|
20
|
+
title: str = Field(default="", description="Page title")
|
|
21
|
+
markdown: str = Field(default="", description="Clean markdown content")
|
|
22
|
+
html: str = Field(default="", description="Raw HTML content")
|
|
23
|
+
links: list[str] = Field(default_factory=list, description="Extracted links")
|
|
24
|
+
images: list[str] = Field(default_factory=list, description="Extracted image URLs")
|
|
25
|
+
metadata: dict[str, Any] = Field(
|
|
26
|
+
default_factory=dict, description="Additional metadata"
|
|
27
|
+
)
|
|
28
|
+
success: bool = Field(default=True, description="Whether crawl was successful")
|
|
29
|
+
error_message: str = Field(default="", description="Error message if failed")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ExtractionResult(BaseModel):
|
|
33
|
+
"""Represents structured data extracted from crawled content."""
|
|
34
|
+
|
|
35
|
+
url: str = Field(description="Source URL")
|
|
36
|
+
extracted_data: Any = Field(description="Extracted structured data")
|
|
37
|
+
extraction_method: str = Field(description="Method used for extraction")
|
|
38
|
+
confidence: float | None = Field(
|
|
39
|
+
default=None, description="Confidence score if available"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WebCrawler:
|
|
44
|
+
"""AI-optimized web crawler using Crawl4AI."""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
headless: bool = True,
|
|
49
|
+
browser_type: str = "chromium",
|
|
50
|
+
user_agent: str | None = None,
|
|
51
|
+
proxy: str | None = None,
|
|
52
|
+
timeout: int = 30,
|
|
53
|
+
delay: float = 1.0,
|
|
54
|
+
verbose: bool = False,
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize the web crawler.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
headless: Run browser in headless mode
|
|
61
|
+
browser_type: Browser type ('chromium', 'firefox', 'webkit')
|
|
62
|
+
user_agent: Custom user agent string
|
|
63
|
+
proxy: Proxy configuration
|
|
64
|
+
timeout: Request timeout in seconds
|
|
65
|
+
delay: Delay between requests in seconds
|
|
66
|
+
verbose: Show Crawl4AI progress output
|
|
67
|
+
**kwargs: Additional configuration for AsyncWebCrawler
|
|
68
|
+
"""
|
|
69
|
+
self.headless = headless
|
|
70
|
+
self.browser_type = browser_type
|
|
71
|
+
self.user_agent = user_agent
|
|
72
|
+
self.proxy = proxy
|
|
73
|
+
self.timeout = timeout
|
|
74
|
+
self.delay = delay
|
|
75
|
+
self.verbose = verbose
|
|
76
|
+
self.config = kwargs
|
|
77
|
+
self._crawler: AsyncWebCrawler | None = None
|
|
78
|
+
|
|
79
|
+
async def __aenter__(self) -> WebCrawler:
|
|
80
|
+
"""Async context manager entry."""
|
|
81
|
+
await self.start()
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
85
|
+
"""Async context manager exit."""
|
|
86
|
+
await self.close()
|
|
87
|
+
|
|
88
|
+
async def start(self) -> None:
|
|
89
|
+
"""Start the crawler."""
|
|
90
|
+
if self._crawler is None:
|
|
91
|
+
browser_config = BrowserConfig(
|
|
92
|
+
headless=self.headless,
|
|
93
|
+
browser_type=self.browser_type,
|
|
94
|
+
proxy=self.proxy,
|
|
95
|
+
verbose=self.verbose,
|
|
96
|
+
)
|
|
97
|
+
self._crawler = AsyncWebCrawler(config=browser_config, **self.config)
|
|
98
|
+
await self._crawler.start()
|
|
99
|
+
|
|
100
|
+
async def close(self) -> None:
|
|
101
|
+
"""Close the crawler."""
|
|
102
|
+
if self._crawler is not None:
|
|
103
|
+
await self._crawler.close()
|
|
104
|
+
self._crawler = None
|
|
105
|
+
|
|
106
|
+
async def crawl(
|
|
107
|
+
self,
|
|
108
|
+
url: str,
|
|
109
|
+
include_raw_html: bool = False,
|
|
110
|
+
extract_links: bool = True,
|
|
111
|
+
extract_images: bool = True,
|
|
112
|
+
css_selector: str | None = None,
|
|
113
|
+
wait_for: str | None = None,
|
|
114
|
+
screenshot: bool = False,
|
|
115
|
+
**kwargs: Any,
|
|
116
|
+
) -> CrawlResult:
|
|
117
|
+
"""Crawl a single URL and extract content.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
url: URL to crawl
|
|
121
|
+
include_raw_html: Include raw HTML in result
|
|
122
|
+
extract_links: Extract all links from the page
|
|
123
|
+
extract_images: Extract all image URLs
|
|
124
|
+
css_selector: CSS selector to extract specific content
|
|
125
|
+
wait_for: CSS selector or JavaScript condition to wait for
|
|
126
|
+
screenshot: Take a screenshot of the page
|
|
127
|
+
**kwargs: Additional parameters for crawl4ai
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
CrawlResult containing the crawled data
|
|
131
|
+
"""
|
|
132
|
+
if self._crawler is None:
|
|
133
|
+
await self.start()
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
# Configure crawl parameters
|
|
137
|
+
crawl_params = {
|
|
138
|
+
"css_selector": css_selector,
|
|
139
|
+
"wait_for": wait_for,
|
|
140
|
+
"screenshot": screenshot,
|
|
141
|
+
**kwargs,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# Remove None values
|
|
145
|
+
crawl_params = {k: v for k, v in crawl_params.items() if v is not None}
|
|
146
|
+
|
|
147
|
+
# Build run config with verbosity setting
|
|
148
|
+
run_config = CrawlerRunConfig(
|
|
149
|
+
verbose=self.verbose,
|
|
150
|
+
log_console=self.verbose,
|
|
151
|
+
**crawl_params,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Perform the crawl
|
|
155
|
+
assert self._crawler is not None
|
|
156
|
+
result = await self._crawler.arun(url=url, config=run_config)
|
|
157
|
+
|
|
158
|
+
# Extract links if requested
|
|
159
|
+
links = []
|
|
160
|
+
if extract_links and hasattr(result, "links") and result.links:
|
|
161
|
+
for link in result.links.get("internal", []):
|
|
162
|
+
href = (
|
|
163
|
+
link.get("href", link) if isinstance(link, dict) else str(link)
|
|
164
|
+
)
|
|
165
|
+
if href:
|
|
166
|
+
links.append(urljoin(url, href))
|
|
167
|
+
for link in result.links.get("external", []):
|
|
168
|
+
href = (
|
|
169
|
+
link.get("href", link) if isinstance(link, dict) else str(link)
|
|
170
|
+
)
|
|
171
|
+
if href:
|
|
172
|
+
links.append(href)
|
|
173
|
+
|
|
174
|
+
# Extract images if requested
|
|
175
|
+
images = []
|
|
176
|
+
if extract_images and hasattr(result, "media") and result.media:
|
|
177
|
+
for img in result.media.get("images", []):
|
|
178
|
+
src = img.get("src", img) if isinstance(img, dict) else str(img)
|
|
179
|
+
if src:
|
|
180
|
+
images.append(urljoin(url, src))
|
|
181
|
+
|
|
182
|
+
# Build metadata
|
|
183
|
+
metadata = {}
|
|
184
|
+
if hasattr(result, "metadata") and result.metadata:
|
|
185
|
+
metadata = result.metadata
|
|
186
|
+
|
|
187
|
+
return CrawlResult(
|
|
188
|
+
url=url,
|
|
189
|
+
title=getattr(result, "title", "") or "",
|
|
190
|
+
markdown=getattr(result, "markdown", "") or "",
|
|
191
|
+
html=(getattr(result, "html", "") or "") if include_raw_html else "",
|
|
192
|
+
links=links,
|
|
193
|
+
images=images,
|
|
194
|
+
metadata=metadata,
|
|
195
|
+
success=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return CrawlResult(url=url, success=False, error_message=str(e))
|
|
200
|
+
|
|
201
|
+
async def crawl_multiple(
|
|
202
|
+
self,
|
|
203
|
+
urls: list[str],
|
|
204
|
+
max_concurrent: int = 3,
|
|
205
|
+
include_raw_html: bool = False,
|
|
206
|
+
extract_links: bool = True,
|
|
207
|
+
extract_images: bool = True,
|
|
208
|
+
**kwargs: Any,
|
|
209
|
+
) -> list[CrawlResult]:
|
|
210
|
+
"""Crawl multiple URLs concurrently.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
urls: List of URLs to crawl
|
|
214
|
+
max_concurrent: Maximum concurrent crawls
|
|
215
|
+
include_raw_html: Include raw HTML in results
|
|
216
|
+
extract_links: Extract links from pages
|
|
217
|
+
extract_images: Extract image URLs
|
|
218
|
+
**kwargs: Additional crawl parameters
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
List of CrawlResult objects
|
|
222
|
+
"""
|
|
223
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
224
|
+
|
|
225
|
+
async def crawl_with_semaphore(url: str) -> CrawlResult:
|
|
226
|
+
async with semaphore:
|
|
227
|
+
if self.delay > 0:
|
|
228
|
+
await asyncio.sleep(self.delay)
|
|
229
|
+
return await self.crawl(
|
|
230
|
+
url=url,
|
|
231
|
+
include_raw_html=include_raw_html,
|
|
232
|
+
extract_links=extract_links,
|
|
233
|
+
extract_images=extract_images,
|
|
234
|
+
**kwargs,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
tasks = [crawl_with_semaphore(url) for url in urls]
|
|
238
|
+
return await asyncio.gather(*tasks)
|
|
239
|
+
|
|
240
|
+
async def extract_structured_data(
|
|
241
|
+
self,
|
|
242
|
+
url: str,
|
|
243
|
+
extraction_strategy: str = "llm",
|
|
244
|
+
schema: dict[str, Any] | None = None,
|
|
245
|
+
instruction: str | None = None,
|
|
246
|
+
llm_model: str | None = None,
|
|
247
|
+
**kwargs: Any,
|
|
248
|
+
) -> ExtractionResult:
|
|
249
|
+
"""Extract structured data from a URL using AI.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
url: URL to extract data from
|
|
253
|
+
extraction_strategy: Strategy to use ('llm', 'cosine')
|
|
254
|
+
schema: JSON schema for structured extraction
|
|
255
|
+
instruction: Extraction instruction for LLM
|
|
256
|
+
llm_model: LLM model to use for extraction
|
|
257
|
+
**kwargs: Additional parameters
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
ExtractionResult with structured data
|
|
261
|
+
"""
|
|
262
|
+
if self._crawler is None:
|
|
263
|
+
await self.start()
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# Configure extraction strategy
|
|
267
|
+
if extraction_strategy == "llm" and (instruction or schema):
|
|
268
|
+
strategy = LLMExtractionStrategy(
|
|
269
|
+
provider="ollama", # Can be configured
|
|
270
|
+
api_token=None,
|
|
271
|
+
instruction=instruction,
|
|
272
|
+
schema=schema,
|
|
273
|
+
**kwargs,
|
|
274
|
+
)
|
|
275
|
+
elif extraction_strategy == "cosine":
|
|
276
|
+
strategy = CosineStrategy(
|
|
277
|
+
semantic_filter=instruction or "relevant content", **kwargs
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
# Default to simple crawl
|
|
281
|
+
strategy = None
|
|
282
|
+
|
|
283
|
+
# Perform extraction
|
|
284
|
+
assert self._crawler is not None
|
|
285
|
+
if strategy:
|
|
286
|
+
result = await self._crawler.arun(url=url, extraction_strategy=strategy)
|
|
287
|
+
extracted_data = getattr(result, "extracted_content", None)
|
|
288
|
+
else:
|
|
289
|
+
result = await self._crawler.arun(url=url)
|
|
290
|
+
extracted_data = getattr(result, "markdown", "")
|
|
291
|
+
|
|
292
|
+
return ExtractionResult(
|
|
293
|
+
url=url,
|
|
294
|
+
extracted_data=extracted_data,
|
|
295
|
+
extraction_method=extraction_strategy,
|
|
296
|
+
confidence=getattr(result, "confidence", None),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
raise LLMError(f"Structured extraction failed for {url}: {e}") from e
|
|
301
|
+
|
|
302
|
+
async def crawl_with_pagination(
|
|
303
|
+
self,
|
|
304
|
+
base_url: str,
|
|
305
|
+
max_pages: int = 5,
|
|
306
|
+
pagination_selector: str | None = None,
|
|
307
|
+
**kwargs: Any,
|
|
308
|
+
) -> list[CrawlResult]:
|
|
309
|
+
"""Crawl a paginated website.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
base_url: Starting URL
|
|
313
|
+
max_pages: Maximum pages to crawl
|
|
314
|
+
pagination_selector: CSS selector for next page link
|
|
315
|
+
**kwargs: Additional crawl parameters
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
List of results from all pages
|
|
319
|
+
"""
|
|
320
|
+
results = []
|
|
321
|
+
current_url = base_url
|
|
322
|
+
|
|
323
|
+
for _page_num in range(max_pages):
|
|
324
|
+
result = await self.crawl(current_url, **kwargs)
|
|
325
|
+
results.append(result)
|
|
326
|
+
|
|
327
|
+
if not result.success:
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
# Find next page URL if pagination selector provided
|
|
331
|
+
if pagination_selector and result.links:
|
|
332
|
+
# Simple logic - would need more sophisticated parsing
|
|
333
|
+
# for complex pagination patterns
|
|
334
|
+
next_urls = [
|
|
335
|
+
link
|
|
336
|
+
for link in result.links
|
|
337
|
+
if "next" in link.lower() or "page" in link.lower()
|
|
338
|
+
]
|
|
339
|
+
if not next_urls:
|
|
340
|
+
break
|
|
341
|
+
current_url = next_urls[0]
|
|
342
|
+
else:
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
return results
|
|
346
|
+
|
|
347
|
+
async def smart_crawl(
|
|
348
|
+
self,
|
|
349
|
+
url: str,
|
|
350
|
+
content_type: str = "article",
|
|
351
|
+
extract_main_content: bool = True,
|
|
352
|
+
remove_noise: bool = True,
|
|
353
|
+
**kwargs: Any,
|
|
354
|
+
) -> CrawlResult:
|
|
355
|
+
"""Intelligent crawling with content optimization.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
url: URL to crawl
|
|
359
|
+
content_type: Type of content expected ('article', 'product', 'blog')
|
|
360
|
+
extract_main_content: Focus on main content area
|
|
361
|
+
remove_noise: Remove ads, navigation, etc.
|
|
362
|
+
**kwargs: Additional parameters
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Optimized CrawlResult
|
|
366
|
+
"""
|
|
367
|
+
# Configure smart extraction based on content type
|
|
368
|
+
smart_params: dict[str, Any] = {}
|
|
369
|
+
|
|
370
|
+
if content_type == "article":
|
|
371
|
+
smart_params["css_selector"] = "article, .content, .post, main"
|
|
372
|
+
elif content_type == "product":
|
|
373
|
+
smart_params["css_selector"] = ".product, .item, .listing"
|
|
374
|
+
elif content_type == "blog":
|
|
375
|
+
smart_params["css_selector"] = ".post, .entry, .blog-content"
|
|
376
|
+
|
|
377
|
+
if remove_noise:
|
|
378
|
+
smart_params["excluded_tags"] = ["nav", "footer", "aside", "advertisement"]
|
|
379
|
+
|
|
380
|
+
return await self.crawl(url=url, **{**smart_params, **kwargs})
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Agent Tools
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def web_crawler_tools(
|
|
388
|
+
text_processor: Any | None = None,
|
|
389
|
+
max_content_tokens: int | None = None,
|
|
390
|
+
) -> ToolRegistry:
|
|
391
|
+
"""Create web crawler tools for agents.
|
|
392
|
+
|
|
393
|
+
These tools are returned as plain async callables. Framework-specific
|
|
394
|
+
backends adapt them when needed.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
text_processor: Optional TextProcessor for handling long crawled content.
|
|
398
|
+
If not provided but max_content_tokens is set,
|
|
399
|
+
a default processor will be created.
|
|
400
|
+
max_content_tokens: Maximum tokens for crawled content. If None, no truncation.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Dict of plain tool callables ready for adaptation by agent frameworks
|
|
404
|
+
|
|
405
|
+
Example:
|
|
406
|
+
>>> from ellements.core.chunking import TextProcessor
|
|
407
|
+
>>> processor = TextProcessor(max_tokens=4000)
|
|
408
|
+
>>> tools = web_crawler_tools(text_processor=processor)
|
|
409
|
+
>>> agent = AgentBuilder("WebCrawler").with_tools(tools).build()
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
if text_processor is None and max_content_tokens is not None:
|
|
413
|
+
from ellements.core.chunking import TextProcessor
|
|
414
|
+
|
|
415
|
+
text_processor = TextProcessor(
|
|
416
|
+
max_tokens=max_content_tokens,
|
|
417
|
+
strategy="simple",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
crawler = WebCrawler()
|
|
421
|
+
|
|
422
|
+
async def crawl_url(url: str) -> str:
|
|
423
|
+
"""Crawl a web page and extract its main content as markdown.
|
|
424
|
+
|
|
425
|
+
This function crawls the specified URL and returns clean markdown content,
|
|
426
|
+
automatically handling long content according to the configured text processing strategy.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
url: URL of the web page to crawl
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Markdown content from the web page (truncated if configured)
|
|
433
|
+
"""
|
|
434
|
+
try:
|
|
435
|
+
async with crawler:
|
|
436
|
+
result = await crawler.crawl(url)
|
|
437
|
+
except Exception as exc:
|
|
438
|
+
return f"Failed to crawl {url}: {exc}"
|
|
439
|
+
|
|
440
|
+
if not result.success:
|
|
441
|
+
return f"Failed to crawl {url}: {result.error_message}"
|
|
442
|
+
|
|
443
|
+
content = result.markdown
|
|
444
|
+
|
|
445
|
+
if text_processor is not None and content:
|
|
446
|
+
content = text_processor.process(content)
|
|
447
|
+
|
|
448
|
+
return content
|
|
449
|
+
|
|
450
|
+
return ToolRegistry.from_mapping(
|
|
451
|
+
{
|
|
452
|
+
"crawl_url": crawl_url,
|
|
453
|
+
}
|
|
454
|
+
)
|