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.
Files changed (130) hide show
  1. ellements/__init__.py +57 -0
  2. ellements/agents/__init__.py +45 -0
  3. ellements/agents/backend.py +100 -0
  4. ellements/agents/builder.py +303 -0
  5. ellements/agents/claude_backend.py +200 -0
  6. ellements/agents/controller.py +237 -0
  7. ellements/agents/openai_backend.py +187 -0
  8. ellements/agents/py.typed +0 -0
  9. ellements/agents/runner.py +358 -0
  10. ellements/agents/tools.py +30 -0
  11. ellements/benchmarking/__init__.py +52 -0
  12. ellements/benchmarking/harness.py +342 -0
  13. ellements/benchmarking/py.typed +0 -0
  14. ellements/benchmarking/results.py +173 -0
  15. ellements/cli/__init__.py +80 -0
  16. ellements/cli/adapters.py +73 -0
  17. ellements/cli/agent_tui.py +1178 -0
  18. ellements/cli/components.py +411 -0
  19. ellements/cli/printer.py +229 -0
  20. ellements/cli/py.typed +0 -0
  21. ellements/core/__init__.py +112 -0
  22. ellements/core/async_utils.py +42 -0
  23. ellements/core/budgeting/__init__.py +43 -0
  24. ellements/core/budgeting/client.py +276 -0
  25. ellements/core/budgeting/protocol.py +51 -0
  26. ellements/core/budgeting/trackers.py +177 -0
  27. ellements/core/caching/__init__.py +51 -0
  28. ellements/core/caching/cache.py +73 -0
  29. ellements/core/caching/client.py +300 -0
  30. ellements/core/caching/disk.py +128 -0
  31. ellements/core/caching/keys.py +97 -0
  32. ellements/core/caching/memory.py +104 -0
  33. ellements/core/chunking.py +262 -0
  34. ellements/core/config.py +180 -0
  35. ellements/core/exceptions.py +145 -0
  36. ellements/core/llm/__init__.py +46 -0
  37. ellements/core/llm/client.py +1124 -0
  38. ellements/core/llm/images.py +226 -0
  39. ellements/core/llm/messages.py +202 -0
  40. ellements/core/llm/model_params.py +66 -0
  41. ellements/core/llm/protocol.py +146 -0
  42. ellements/core/llm/requests.py +100 -0
  43. ellements/core/llm/structured.py +91 -0
  44. ellements/core/llm/wrapper.py +79 -0
  45. ellements/core/observability/__init__.py +39 -0
  46. ellements/core/observability/events.py +169 -0
  47. ellements/core/observability/jsonl_logger.py +244 -0
  48. ellements/core/observability/markdown_formatter.py +197 -0
  49. ellements/core/observability/observer.py +56 -0
  50. ellements/core/prompting/__init__.py +14 -0
  51. ellements/core/prompting/context.py +185 -0
  52. ellements/core/prompting/guideline.py +133 -0
  53. ellements/core/prompting/persona.py +267 -0
  54. ellements/core/prompting/sources.py +92 -0
  55. ellements/core/py.typed +0 -0
  56. ellements/core/rate_limit/__init__.py +44 -0
  57. ellements/core/rate_limit/bucket.py +85 -0
  58. ellements/core/rate_limit/client.py +216 -0
  59. ellements/core/rate_limit/protocol.py +27 -0
  60. ellements/core/templating.py +126 -0
  61. ellements/core/tools/__init__.py +51 -0
  62. ellements/core/tools/dialects.py +136 -0
  63. ellements/core/tools/executor.py +48 -0
  64. ellements/core/tools/protocol.py +36 -0
  65. ellements/core/tools/records.py +28 -0
  66. ellements/core/tools/registry.py +205 -0
  67. ellements/core/tools/schemas.py +80 -0
  68. ellements/core/tools/simple.py +119 -0
  69. ellements/core/tools/spec.py +33 -0
  70. ellements/domain_specific/__init__.py +7 -0
  71. ellements/domain_specific/finance/__init__.py +188 -0
  72. ellements/domain_specific/finance/calculations.py +837 -0
  73. ellements/domain_specific/finance/charts.py +426 -0
  74. ellements/domain_specific/finance/portfolio.py +129 -0
  75. ellements/domain_specific/finance/quant_analysis.py +279 -0
  76. ellements/domain_specific/finance/risk.py +362 -0
  77. ellements/domain_specific/finance/technical_indicators.py +241 -0
  78. ellements/domain_specific/finance/tools.py +483 -0
  79. ellements/domain_specific/finance/valuation.py +312 -0
  80. ellements/domain_specific/finance/yahoo_finance.py +1523 -0
  81. ellements/domain_specific/finance/yahoo_finance_models.py +321 -0
  82. ellements/domain_specific/py.typed +0 -0
  83. ellements/execution/__init__.py +56 -0
  84. ellements/execution/callbacks.py +149 -0
  85. ellements/execution/catalog.py +70 -0
  86. ellements/execution/collaborative.py +191 -0
  87. ellements/execution/config.py +135 -0
  88. ellements/execution/py.typed +0 -0
  89. ellements/execution/reflection.py +156 -0
  90. ellements/execution/self_consistency.py +189 -0
  91. ellements/execution/single_call.py +48 -0
  92. ellements/execution/strategies.py +237 -0
  93. ellements/execution/tree_of_thought.py +541 -0
  94. ellements/fslm/__init__.py +108 -0
  95. ellements/fslm/builtins.py +103 -0
  96. ellements/fslm/cli.py +199 -0
  97. ellements/fslm/context.py +50 -0
  98. ellements/fslm/definition.py +163 -0
  99. ellements/fslm/det.py +173 -0
  100. ellements/fslm/dsl.py +458 -0
  101. ellements/fslm/errors.py +38 -0
  102. ellements/fslm/evaluators.py +141 -0
  103. ellements/fslm/kernel.py +603 -0
  104. ellements/fslm/loading.py +123 -0
  105. ellements/fslm/models.py +623 -0
  106. ellements/fslm/nl.py +87 -0
  107. ellements/fslm/observers.py +107 -0
  108. ellements/fslm/persistence.py +72 -0
  109. ellements/fslm/py.typed +0 -0
  110. ellements/fslm/rendering.py +551 -0
  111. ellements/fslm/visualization.py +25 -0
  112. ellements/reporting/__init__.py +12 -0
  113. ellements/reporting/charts.py +364 -0
  114. ellements/reporting/html_generation.py +132 -0
  115. ellements/reporting/py.typed +0 -0
  116. ellements/reporting/visualization.py +73 -0
  117. ellements/standard_tools/__init__.py +22 -0
  118. ellements/standard_tools/py.typed +0 -0
  119. ellements/standard_tools/terminal.py +205 -0
  120. ellements/standard_tools/web/__init__.py +37 -0
  121. ellements/standard_tools/web/browser_viewer.py +214 -0
  122. ellements/standard_tools/web/crawler.py +454 -0
  123. ellements/standard_tools/web/search.py +399 -0
  124. ellements/standard_tools/web/youtube.py +1297 -0
  125. ellements-0.2.0.dist-info/METADATA +368 -0
  126. ellements-0.2.0.dist-info/RECORD +130 -0
  127. ellements-0.2.0.dist-info/WHEEL +5 -0
  128. ellements-0.2.0.dist-info/entry_points.txt +2 -0
  129. ellements-0.2.0.dist-info/licenses/LICENSE +42 -0
  130. 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
+ )