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,358 @@
1
+ """Run agents through any :class:`AgentBackend` with progress + stats.
2
+
3
+ The runner is backend-agnostic: it consumes the uniform
4
+ :class:`~ellements.agents.AgentEvent` vocabulary emitted by
5
+ :meth:`AgentBackend.stream_run`. Stats are kept in a clean dict-backed
6
+ :class:`AgentRunStats` model — there are no legacy ``searches`` /
7
+ ``results_examined`` aliases. Callers that want them increment the
8
+ metric explicitly via :meth:`AgentRunStats.increment_metric`.
9
+
10
+ The single entry point is :func:`run_agent_with_progress`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ from collections.abc import Awaitable, Callable
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+ from .backend import AgentBackend, AgentEvent
22
+
23
+ DEFAULT_MAX_TURNS = 200
24
+
25
+ ProgressCallback = Callable[[str], Awaitable[None] | None]
26
+ StatsCallback = Callable[["AgentRunStats"], Awaitable[None] | None]
27
+
28
+
29
+ # ── Stats ──────────────────────────────────────────────────────────
30
+
31
+
32
+ @dataclass
33
+ class AgentRunStats:
34
+ """Statistics gathered during one agent run."""
35
+
36
+ tool_calls: int = 0
37
+ tool_outputs: int = 0
38
+ observed_items: int = 0
39
+ tool_names: dict[str, int] = field(default_factory=dict)
40
+ metrics: dict[str, int] = field(default_factory=dict)
41
+
42
+ def increment_metric(self, name: str, amount: int = 1) -> None:
43
+ self.metrics[name] = self.metrics.get(name, 0) + amount
44
+
45
+ def increment_tool_call(self, tool_name: str) -> None:
46
+ self.tool_calls += 1
47
+ clean = (tool_name or "unknown").strip() or "unknown"
48
+ self.tool_names[clean] = self.tool_names.get(clean, 0) + 1
49
+
50
+ def increment_tool_output(self, amount: int = 1) -> None:
51
+ self.tool_outputs += amount
52
+
53
+ def observe_items(self, count: int) -> None:
54
+ if count > 0:
55
+ self.observed_items += count
56
+
57
+ def merge(self, other: AgentRunStats) -> None:
58
+ self.tool_calls += other.tool_calls
59
+ self.tool_outputs += other.tool_outputs
60
+ self.observed_items += other.observed_items
61
+ for name, count in other.tool_names.items():
62
+ self.tool_names[name] = self.tool_names.get(name, 0) + count
63
+ for name, count in other.metrics.items():
64
+ self.metrics[name] = self.metrics.get(name, 0) + count
65
+
66
+ def to_dict(self) -> dict[str, Any]:
67
+ return {
68
+ "tool_calls": self.tool_calls,
69
+ "tool_outputs": self.tool_outputs,
70
+ "observed_items": self.observed_items,
71
+ "tool_names": dict(self.tool_names),
72
+ "metrics": dict(self.metrics),
73
+ }
74
+
75
+
76
+ @dataclass
77
+ class AgentRunResult:
78
+ """Wrapper exposing the backend-native result plus collected stats.
79
+
80
+ Attributes:
81
+ result: The raw backend-native result object. Inspect this
82
+ directly to access provider-specific data (e.g. token
83
+ usage, raw items, tool-call traces).
84
+ stats: Uniform :class:`AgentRunStats` collected during the
85
+ run, regardless of which backend produced *result*.
86
+
87
+ The ``final_output`` property is the canonical, backend-agnostic
88
+ accessor for the assistant's final text response. Anything else
89
+ must go through ``result.result.<native_attr>`` — the wrapper
90
+ intentionally does **not** proxy unknown attributes to keep the
91
+ public surface explicit and discoverable.
92
+ """
93
+
94
+ result: Any
95
+ stats: AgentRunStats
96
+
97
+ @property
98
+ def final_output(self) -> str:
99
+ """Return the assistant's final text reply (``""`` if missing)."""
100
+ return getattr(self.result, "final_output", "")
101
+
102
+
103
+ # ── Public entry point ─────────────────────────────────────────────
104
+
105
+
106
+ async def run_agent_with_progress(
107
+ backend: AgentBackend,
108
+ agent: Any,
109
+ query: str,
110
+ *,
111
+ progress_callback: ProgressCallback | None = None,
112
+ stats_callback: StatsCallback | None = None,
113
+ max_turns: int = DEFAULT_MAX_TURNS,
114
+ session: Any | None = None,
115
+ show_activity_log: bool = True,
116
+ ) -> AgentRunResult:
117
+ """Run *agent* through *backend* and report progress incrementally.
118
+
119
+ When ``show_activity_log`` is True the runner uses
120
+ :meth:`AgentBackend.stream_run`; otherwise it uses
121
+ :meth:`AgentBackend.run`.
122
+ """
123
+ stats = AgentRunStats()
124
+ await _emit(
125
+ progress_callback,
126
+ "🚀 Starting agent…\n"
127
+ f" [cyan]Backend:[/cyan] {backend.name}\n"
128
+ f" [cyan]Max turns:[/cyan] {max_turns}\n"
129
+ f" [cyan]Activity log:[/cyan] "
130
+ f"{'enabled' if show_activity_log else 'disabled'}\n"
131
+ f" [cyan]Model:[/cyan] {getattr(agent, 'model', 'default')}\n",
132
+ )
133
+
134
+ if show_activity_log and progress_callback is not None:
135
+ stream = backend.stream_run(
136
+ agent, query, max_turns=max_turns, session=session
137
+ )
138
+ await _emit(
139
+ progress_callback,
140
+ "\n[bold cyan]📋 Agent Activity Log:[/bold cyan]\n",
141
+ )
142
+ try:
143
+ async for event in stream:
144
+ await _handle_event(
145
+ event, stats, progress_callback, stats_callback
146
+ )
147
+ except asyncio.CancelledError:
148
+ await _emit(
149
+ progress_callback,
150
+ "\n[bold red]🛑 Streaming cancelled[/bold red]",
151
+ )
152
+ raise
153
+ result = stream.result
154
+ else:
155
+ result = await backend.run(
156
+ agent, query, max_turns=max_turns, session=session
157
+ )
158
+ _update_stats_from_native(stats, result)
159
+
160
+ await _emit(
161
+ progress_callback,
162
+ "\n\n[bold green]🎉 Final Answer[/bold green]\n",
163
+ )
164
+ return AgentRunResult(result=result, stats=stats)
165
+
166
+
167
+ # ── Helpers ────────────────────────────────────────────────────────
168
+
169
+
170
+ async def _handle_event(
171
+ event: AgentEvent,
172
+ stats: AgentRunStats,
173
+ progress_callback: ProgressCallback | None,
174
+ stats_callback: StatsCallback | None,
175
+ ) -> None:
176
+ """Dispatch a single :class:`AgentEvent` to its per-type handler."""
177
+ handler = _EVENT_HANDLERS.get(event.type)
178
+ if handler is None:
179
+ return
180
+ await handler(event, stats, progress_callback, stats_callback)
181
+
182
+
183
+ async def _on_agent_active(
184
+ event: AgentEvent,
185
+ _stats: AgentRunStats,
186
+ progress_callback: ProgressCallback | None,
187
+ _stats_callback: StatsCallback | None,
188
+ ) -> None:
189
+ await _emit(
190
+ progress_callback,
191
+ f"\n[bold]🔄 Agent active:[/bold] "
192
+ f"[cyan]{event.payload.get('name', 'Agent')}[/cyan]",
193
+ )
194
+
195
+
196
+ async def _on_tool_call(
197
+ event: AgentEvent,
198
+ stats: AgentRunStats,
199
+ progress_callback: ProgressCallback | None,
200
+ stats_callback: StatsCallback | None,
201
+ ) -> None:
202
+ name = str(event.payload.get("name", "unknown"))
203
+ args = event.payload.get("arguments", "{}")
204
+ stats.increment_tool_call(name)
205
+ await _emit_stats(stats_callback, stats)
206
+ await _emit(
207
+ progress_callback,
208
+ f"\n[bold yellow]🔧 Tool:[/bold yellow] [magenta]{name}[/magenta]",
209
+ )
210
+ formatted = _format_tool_args(args)
211
+ if formatted:
212
+ await _emit(progress_callback, f" {formatted}")
213
+
214
+
215
+ async def _on_tool_output(
216
+ event: AgentEvent,
217
+ stats: AgentRunStats,
218
+ progress_callback: ProgressCallback | None,
219
+ stats_callback: StatsCallback | None,
220
+ ) -> None:
221
+ output = event.payload.get("output")
222
+ stats.increment_tool_output()
223
+ result_count = _count_results(output)
224
+ if result_count:
225
+ stats.observe_items(result_count)
226
+ await _emit_stats(stats_callback, stats)
227
+ await _emit(progress_callback, "\n[bold green]✅ Result[/bold green]")
228
+ for line in _format_tool_output(output):
229
+ await _emit(progress_callback, f" {line}")
230
+
231
+
232
+ async def _on_message(
233
+ event: AgentEvent,
234
+ _stats: AgentRunStats,
235
+ progress_callback: ProgressCallback | None,
236
+ _stats_callback: StatsCallback | None,
237
+ ) -> None:
238
+ text = str(event.payload.get("text", ""))
239
+ if not text:
240
+ return
241
+ if len(text) < 500:
242
+ await _emit(
243
+ progress_callback,
244
+ f"\n[bold blue]💭 Thinking[/bold blue]\n [dim]{text}[/dim]",
245
+ )
246
+ else:
247
+ preview = text[:150].replace("\n", " ")
248
+ await _emit(
249
+ progress_callback,
250
+ f"\n[bold blue]💭 Composing response:[/bold blue] "
251
+ f"[dim]{preview}…[/dim]",
252
+ )
253
+
254
+
255
+ _EventHandler = Callable[
256
+ [AgentEvent, AgentRunStats, ProgressCallback | None, StatsCallback | None],
257
+ Awaitable[None],
258
+ ]
259
+
260
+ # Per-event-type dispatch table for ``_handle_event``. Adding a new
261
+ # event type means appending one entry here rather than threading
262
+ # another ``elif`` branch through a growing if-cascade.
263
+ _EVENT_HANDLERS: dict[str, _EventHandler] = {
264
+ "agent_active": _on_agent_active,
265
+ "tool_call": _on_tool_call,
266
+ "tool_output": _on_tool_output,
267
+ "message": _on_message,
268
+ }
269
+
270
+
271
+ async def _emit(cb: ProgressCallback | None, message: str) -> None:
272
+ if cb is None:
273
+ return
274
+ result = cb(message)
275
+ if asyncio.iscoroutine(result):
276
+ await result
277
+
278
+
279
+ async def _emit_stats(
280
+ cb: StatsCallback | None, stats: AgentRunStats
281
+ ) -> None:
282
+ if cb is None:
283
+ return
284
+ result = cb(stats)
285
+ if asyncio.iscoroutine(result):
286
+ await result
287
+
288
+
289
+ def _update_stats_from_native(stats: AgentRunStats, result: Any) -> None:
290
+ """Populate stats from native-result items when streaming was disabled."""
291
+ new_items = getattr(result, "new_items", None) or []
292
+ for item in new_items:
293
+ item_type = getattr(item, "type", None)
294
+ if item_type == "tool_call_item":
295
+ raw_item = getattr(item, "raw_item", None) or item
296
+ stats.increment_tool_call(getattr(raw_item, "name", "unknown"))
297
+ elif item_type == "tool_call_output_item":
298
+ stats.increment_tool_output()
299
+ output = getattr(item, "output", None)
300
+ stats.observe_items(_count_results(output))
301
+
302
+
303
+ def _count_results(output: Any) -> int:
304
+ """Best-effort count of result-like items in a tool output."""
305
+ if output is None:
306
+ return 0
307
+ results = getattr(output, "results", None)
308
+ if isinstance(results, list):
309
+ return len(results)
310
+ return 0
311
+
312
+
313
+ def _format_tool_args(args: Any) -> str:
314
+ """Format tool args (JSON string or mapping) into a Rich-styled line."""
315
+ if isinstance(args, str):
316
+ if args in ("", "{}", "null"):
317
+ return ""
318
+ try:
319
+ data = json.loads(args)
320
+ except json.JSONDecodeError:
321
+ return f"[dim]{args}[/dim]"
322
+ elif isinstance(args, dict):
323
+ data = args
324
+ else:
325
+ return ""
326
+ parts: list[str] = []
327
+ for key, value in data.items():
328
+ formatted = (
329
+ f'"{value}"' if isinstance(value, str) else json.dumps(value)
330
+ )
331
+ parts.append(f"[blue]{key}[/blue]=[green]{formatted}[/green]")
332
+ return ", ".join(parts)
333
+
334
+
335
+ def _format_tool_output(output: Any) -> list[str]:
336
+ if output is None:
337
+ return []
338
+ if isinstance(output, str):
339
+ if len(output) > 200:
340
+ return [
341
+ f"[cyan]Text:[/cyan] {len(output.split())} words",
342
+ f"[dim]Preview:[/dim] {output[:150]}…",
343
+ ]
344
+ return [f"[green]{output}[/green]"]
345
+ text = str(output)
346
+ if len(text) > 300:
347
+ return [f"[dim]{text[:300]}…[/dim]"]
348
+ return [f"[dim]{text}[/dim]"]
349
+
350
+
351
+ __all__ = [
352
+ "AgentRunResult",
353
+ "AgentRunStats",
354
+ "DEFAULT_MAX_TURNS",
355
+ "ProgressCallback",
356
+ "StatsCallback",
357
+ "run_agent_with_progress",
358
+ ]
@@ -0,0 +1,30 @@
1
+ """Generic tool-creation utility shared by all agent backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from ellements.core import SimpleTool
10
+
11
+
12
+ def create_tool_from_method(
13
+ method: Callable[..., Any],
14
+ *,
15
+ name: str | None = None,
16
+ description: str | None = None,
17
+ ) -> SimpleTool:
18
+ """Wrap a method as a canonical :class:`~ellements.core.SimpleTool`.
19
+
20
+ The result is backend-agnostic; backends adapt it to their native
21
+ tool type at agent-construction time.
22
+ """
23
+ tool_name = name or method.__name__
24
+ tool_description = description or (
25
+ inspect.getdoc(method) or f"Execute {tool_name}"
26
+ )
27
+ return SimpleTool(method, name=tool_name, description=tool_description)
28
+
29
+
30
+ __all__ = ["create_tool_from_method"]
@@ -0,0 +1,52 @@
1
+ """Benchmarking module — evaluate prompting strategies against standard LLM
2
+ benchmarks via `lm-evaluation-harness <https://github.com/EleutherAI/lm-evaluation-harness>`_.
3
+
4
+ Quick start::
5
+
6
+ from ellements.benchmarking import run_benchmark, compare_benchmarks
7
+ from ellements.execution import (
8
+ SelfConsistencyConfig,
9
+ SelfConsistencyStrategy,
10
+ SingleCallStrategy,
11
+ )
12
+
13
+ # Benchmark a model on GSM8K
14
+ results = run_benchmark(model="openai/gpt-4o", tasks=["gsm8k"])
15
+
16
+ # Benchmark with a typed execution strategy config
17
+ results = run_benchmark(
18
+ model="openai/gpt-4o",
19
+ strategy=SelfConsistencyStrategy(),
20
+ strategy_config=SelfConsistencyConfig(samples=5),
21
+ tasks=["gsm8k"],
22
+ )
23
+
24
+ # Compare strategies
25
+ comparison = compare_benchmarks(
26
+ model="openai/gpt-4o",
27
+ strategies={
28
+ "baseline": None,
29
+ "single-call": SingleCallStrategy(),
30
+ },
31
+ tasks=["gsm8k"],
32
+ )
33
+ comparison.print_table()
34
+
35
+ Requires: ``pip install ellements[benchmarking]``
36
+ """
37
+
38
+ from .harness import (
39
+ BenchmarkModel,
40
+ OnBenchmarkProgress,
41
+ compare_benchmarks,
42
+ run_benchmark,
43
+ )
44
+ from .results import BenchmarkComparison
45
+
46
+ __all__ = [
47
+ "BenchmarkComparison",
48
+ "BenchmarkModel",
49
+ "OnBenchmarkProgress",
50
+ "compare_benchmarks",
51
+ "run_benchmark",
52
+ ]