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,399 @@
1
+ """Web search functionality using DDGS with backend fallback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ try:
9
+ from ddgs import DDGS
10
+ except ImportError:
11
+ from duckduckgo_search import DDGS
12
+ from ellements.core import ToolRegistry
13
+ from ellements.core.exceptions import LLMError
14
+ from pydantic import BaseModel, Field
15
+
16
+
17
+ class SearchResult(BaseModel):
18
+ """Represents a single search result."""
19
+
20
+ title: str = Field(description="Title of the search result")
21
+ url: str = Field(description="URL of the search result")
22
+ snippet: str = Field(description="Brief description/snippet of the result")
23
+ source: str = Field(default="google", description="Search backend source")
24
+
25
+
26
+ class SearchResults(BaseModel):
27
+ """Container for multiple search results."""
28
+
29
+ query: str = Field(description="The search query that was executed")
30
+ results: list[SearchResult] = Field(description="List of search results")
31
+ total_results: int = Field(description="Total number of results returned")
32
+
33
+ def __len__(self) -> int:
34
+ """Return the number of results."""
35
+ return len(self.results)
36
+
37
+ def __iter__(self) -> Any:
38
+ """Allow iteration over results."""
39
+ return iter(self.results)
40
+
41
+ def __getitem__(self, index: int) -> SearchResult:
42
+ """Allow indexing of results."""
43
+ return self.results[index]
44
+
45
+
46
+ class WebSearcher:
47
+ """Web search client using DDGS."""
48
+
49
+ _TEXT_BACKEND_DEFAULTS = ("google", "duckduckgo", "bing", "auto", "html", "lite")
50
+ _NEWS_BACKEND_DEFAULTS = ("duckduckgo", "bing", "yahoo", "auto", "html", "lite")
51
+
52
+ def __init__(
53
+ self,
54
+ region: str = "us-en",
55
+ safe_search: str = "moderate",
56
+ timeout: int = 10,
57
+ max_results: int = 10,
58
+ **kwargs: Any,
59
+ ) -> None:
60
+ """Initialize the web searcher.
61
+
62
+ Args:
63
+ region: Region for search results (e.g., 'us-en', 'uk-en', 'de-de')
64
+ safe_search: Safe search setting ('strict', 'moderate', 'off')
65
+ timeout: Request timeout in seconds
66
+ max_results: Maximum number of results to return
67
+ **kwargs: Additional configuration for DDGS
68
+ """
69
+ self.region = region
70
+ self.safe_search = safe_search
71
+ self.timeout = timeout
72
+ self.max_results = max_results
73
+ self.config = kwargs
74
+
75
+ def _backend_candidates(self, category: str, backend: str | None) -> list[str]:
76
+ if backend:
77
+ return [backend]
78
+ defaults = (
79
+ self._TEXT_BACKEND_DEFAULTS
80
+ if category == "text"
81
+ else self._NEWS_BACKEND_DEFAULTS
82
+ )
83
+ seen: set[str] = set()
84
+ candidates: list[str] = []
85
+ for candidate in defaults:
86
+ if candidate in seen:
87
+ continue
88
+ seen.add(candidate)
89
+ candidates.append(candidate)
90
+ return candidates
91
+
92
+ def _run_ddgs_query(
93
+ self,
94
+ ddgs: DDGS,
95
+ category: str,
96
+ query: str,
97
+ region: str,
98
+ safesearch: str,
99
+ timelimit: str | None,
100
+ max_results: int,
101
+ backend: str,
102
+ **kwargs: Any,
103
+ ) -> list[dict[str, Any]]:
104
+ method = ddgs.text if category == "text" else ddgs.news
105
+ common_kwargs = {
106
+ "region": region,
107
+ "safesearch": safesearch,
108
+ "timelimit": timelimit,
109
+ "max_results": max_results,
110
+ "backend": backend,
111
+ **kwargs,
112
+ }
113
+ try:
114
+ return list(method(query, **common_kwargs))
115
+ except TypeError:
116
+ return list(method(keywords=query, **common_kwargs))
117
+
118
+ def search(
119
+ self,
120
+ query: str,
121
+ max_results: int | None = None,
122
+ region: str | None = None,
123
+ safe_search: str | None = None,
124
+ time_range: str | None = None,
125
+ backend: str | None = None,
126
+ **kwargs: Any,
127
+ ) -> SearchResults:
128
+ """Perform a web search.
129
+
130
+ Args:
131
+ query: Search query string
132
+ max_results: Maximum number of results (overrides default)
133
+ region: Region override
134
+ safe_search: Safe search override
135
+ time_range: Time range filter ('d', 'w', 'm', 'y' for day, week, month, year)
136
+ **kwargs: Additional search parameters
137
+
138
+ Returns:
139
+ SearchResults object containing the results
140
+ """
141
+ try:
142
+ # Use provided parameters or fall back to instance defaults
143
+ search_region = region or self.region
144
+ search_safe = safe_search or self.safe_search
145
+ search_max = max_results or self.max_results
146
+
147
+ backend_candidates = self._backend_candidates("text", backend)
148
+ last_error: Exception | None = None
149
+
150
+ with DDGS(timeout=self.timeout, **self.config) as ddgs:
151
+ for candidate in backend_candidates:
152
+ try:
153
+ raw_results = self._run_ddgs_query(
154
+ ddgs,
155
+ "text",
156
+ query=query,
157
+ region=search_region,
158
+ safesearch=search_safe,
159
+ timelimit=time_range,
160
+ max_results=search_max,
161
+ backend=candidate,
162
+ **kwargs,
163
+ )
164
+ except Exception as exc:
165
+ last_error = exc
166
+ continue
167
+
168
+ results = []
169
+ for result in raw_results:
170
+ search_result = SearchResult(
171
+ title=result.get("title", ""),
172
+ url=result.get("href", ""),
173
+ snippet=result.get("body", ""),
174
+ source=candidate,
175
+ )
176
+ results.append(search_result)
177
+
178
+ return SearchResults(
179
+ query=query, results=results, total_results=len(results)
180
+ )
181
+
182
+ if last_error is not None:
183
+ raise last_error
184
+ return SearchResults(query=query, results=[], total_results=0)
185
+
186
+ except Exception as e:
187
+ raise LLMError(f"Web search failed: {e}") from e
188
+
189
+ async def search_async(
190
+ self,
191
+ query: str,
192
+ max_results: int | None = None,
193
+ region: str | None = None,
194
+ safe_search: str | None = None,
195
+ time_range: str | None = None,
196
+ **kwargs: Any,
197
+ ) -> SearchResults:
198
+ """Async version of search."""
199
+ # Run the synchronous search in a thread pool
200
+ loop = asyncio.get_event_loop()
201
+ return await loop.run_in_executor(
202
+ None,
203
+ lambda: self.search(
204
+ query=query,
205
+ max_results=max_results,
206
+ region=region,
207
+ safe_search=safe_search,
208
+ time_range=time_range,
209
+ **kwargs,
210
+ ),
211
+ )
212
+
213
+ def search_news(
214
+ self,
215
+ query: str,
216
+ max_results: int | None = None,
217
+ region: str | None = None,
218
+ safe_search: str | None = None,
219
+ time_range: str | None = None,
220
+ backend: str | None = None,
221
+ **kwargs: Any,
222
+ ) -> SearchResults:
223
+ """Search for news articles.
224
+
225
+ Args:
226
+ query: Search query string
227
+ max_results: Maximum number of results
228
+ region: Region for search
229
+ safe_search: Safe search setting
230
+ time_range: Time range filter
231
+ **kwargs: Additional parameters
232
+
233
+ Returns:
234
+ SearchResults with news articles
235
+ """
236
+ try:
237
+ search_region = region or self.region
238
+ search_safe = safe_search or self.safe_search
239
+ search_max = max_results or self.max_results
240
+
241
+ backend_candidates = self._backend_candidates("news", backend)
242
+ last_error: Exception | None = None
243
+
244
+ with DDGS(timeout=self.timeout, **self.config) as ddgs:
245
+ for candidate in backend_candidates:
246
+ try:
247
+ raw_results = self._run_ddgs_query(
248
+ ddgs,
249
+ "news",
250
+ query=query,
251
+ region=search_region,
252
+ safesearch=search_safe,
253
+ timelimit=time_range,
254
+ max_results=search_max,
255
+ backend=candidate,
256
+ **kwargs,
257
+ )
258
+ except Exception as exc:
259
+ last_error = exc
260
+ continue
261
+
262
+ results = []
263
+ for result in raw_results:
264
+ search_result = SearchResult(
265
+ title=result.get("title", ""),
266
+ url=result.get("url", ""),
267
+ snippet=result.get("body", ""),
268
+ source=f"{candidate}_news",
269
+ )
270
+ results.append(search_result)
271
+
272
+ return SearchResults(
273
+ query=query, results=results, total_results=len(results)
274
+ )
275
+
276
+ if last_error is not None:
277
+ raise last_error
278
+ return SearchResults(query=query, results=[], total_results=0)
279
+
280
+ except Exception as e:
281
+ raise LLMError(f"News search failed: {e}") from e
282
+
283
+ async def search_news_async(
284
+ self,
285
+ query: str,
286
+ max_results: int | None = None,
287
+ region: str | None = None,
288
+ safe_search: str | None = None,
289
+ time_range: str | None = None,
290
+ **kwargs: Any,
291
+ ) -> SearchResults:
292
+ """Async version of news search."""
293
+ loop = asyncio.get_event_loop()
294
+ return await loop.run_in_executor(
295
+ None,
296
+ lambda: self.search_news(
297
+ query=query,
298
+ max_results=max_results,
299
+ region=region,
300
+ safe_search=safe_search,
301
+ time_range=time_range,
302
+ **kwargs,
303
+ ),
304
+ )
305
+
306
+ def quick_search(self, query: str, num_results: int = 5) -> list[str]:
307
+ """Quick search returning just URLs.
308
+
309
+ Args:
310
+ query: Search query
311
+ num_results: Number of results to return
312
+
313
+ Returns:
314
+ List of URLs
315
+ """
316
+ results = self.search(query, max_results=num_results)
317
+ return [result.url for result in results.results]
318
+
319
+ def search_and_summarize(self, query: str, max_results: int = 3) -> str:
320
+ """Search and create a text summary of results.
321
+
322
+ Args:
323
+ query: Search query
324
+ max_results: Number of results to include
325
+
326
+ Returns:
327
+ Formatted text summary
328
+ """
329
+ results = self.search(query, max_results=max_results)
330
+
331
+ summary_lines = [f"Search results for: {query}\n"]
332
+
333
+ for i, result in enumerate(results.results, 1):
334
+ summary_lines.append(f"{i}. {result.title}")
335
+ summary_lines.append(f" URL: {result.url}")
336
+ summary_lines.append(f" {result.snippet}\n")
337
+
338
+ return "\n".join(summary_lines)
339
+
340
+
341
+ # Agent Tools
342
+ # ---------------------------------------------------------------------------
343
+
344
+
345
+ def web_search_tools() -> ToolRegistry:
346
+ """Create web search tools for agents.
347
+
348
+ These tools are returned as plain callables. Framework-specific backends
349
+ adapt them when needed.
350
+
351
+ Returns:
352
+ Dict of plain tool callables ready for adaptation by agent frameworks
353
+
354
+ Example:
355
+ >>> tools = web_search_tools()
356
+ >>> agent = AgentBuilder("WebResearcher").with_tools(tools).build()
357
+ """
358
+
359
+ # Create searcher instance
360
+ searcher = WebSearcher()
361
+
362
+ def search_web(
363
+ query: str, max_results: int = 10, time_range: str | None = None
364
+ ) -> SearchResults:
365
+ """Search the web using DDGS (Google backend by default).
366
+
367
+ Args:
368
+ query: Search query string
369
+ max_results: Maximum number of results to return (default: 10)
370
+ time_range: Filter by time range - 'd' (day), 'w' (week), 'm' (month), 'y' (year)
371
+
372
+ Returns:
373
+ SearchResults object with web pages matching the query
374
+ """
375
+ return searcher.search(query, max_results=max_results, time_range=time_range)
376
+
377
+ def search_news(
378
+ query: str, max_results: int = 10, time_range: str | None = None
379
+ ) -> SearchResults:
380
+ """Search for news articles using DDGS News backends.
381
+
382
+ Args:
383
+ query: Search query string
384
+ max_results: Maximum number of results to return (default: 10)
385
+ time_range: Filter by time range - 'd' (day), 'w' (week), 'm' (month), 'y' (year)
386
+
387
+ Returns:
388
+ SearchResults object with news articles matching the query
389
+ """
390
+ return searcher.search_news(
391
+ query, max_results=max_results, time_range=time_range
392
+ )
393
+
394
+ return ToolRegistry.from_mapping(
395
+ {
396
+ "search_web": search_web,
397
+ "search_news": search_news,
398
+ }
399
+ )