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,364 @@
1
+ """Structured chart generation helpers for Canvas/chat/report surfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import html
7
+ import json
8
+ import math
9
+ from io import BytesIO
10
+ from typing import Any
11
+
12
+ _SUPPORTED_CHART_TYPES = {"line", "bar", "pie", "histogram", "scatter", "area"}
13
+
14
+
15
+ def _load_matplotlib() -> Any:
16
+ try:
17
+ import matplotlib.pyplot as plt
18
+ except Exception as exc: # pragma: no cover - dependency/runtime specific
19
+ raise ValueError(
20
+ "Chart rendering requires matplotlib. Install with: pip install matplotlib"
21
+ ) from exc
22
+ return plt
23
+
24
+
25
+ def _coerce_float(value: Any, field_name: str, *, allow_nan: bool = False) -> float:
26
+ if isinstance(value, bool):
27
+ raise ValueError(f"{field_name} values must be numeric.")
28
+ try:
29
+ parsed = float(value)
30
+ except Exception as exc:
31
+ raise ValueError(f"{field_name} values must be numeric.") from exc
32
+ if math.isnan(parsed):
33
+ if allow_nan:
34
+ return parsed
35
+ raise ValueError(f"{field_name} values must be finite.")
36
+ if math.isinf(parsed):
37
+ raise ValueError(f"{field_name} values must be finite.")
38
+ return parsed
39
+
40
+
41
+ def _coerce_numeric_list(value: Any, field_name: str) -> list[float]:
42
+ if not isinstance(value, list) or not value:
43
+ raise ValueError(f"{field_name} must be a non-empty array.")
44
+ return [_coerce_float(item, field_name, allow_nan=True) for item in value]
45
+
46
+
47
+ def _coerce_x_axis(value: Any, count: int, field_name: str) -> tuple[list[float], list[str] | None]:
48
+ if value is None:
49
+ return [float(index + 1) for index in range(count)], None
50
+ if not isinstance(value, list):
51
+ raise ValueError(f"{field_name} must be an array when provided.")
52
+ if len(value) != count:
53
+ raise ValueError(f"{field_name} length must match y/values length.")
54
+
55
+ numeric_values: list[float] = []
56
+ all_numeric = True
57
+ for item in value:
58
+ try:
59
+ numeric_values.append(_coerce_float(item, field_name))
60
+ except ValueError:
61
+ all_numeric = False
62
+ break
63
+ if all_numeric:
64
+ return numeric_values, None
65
+ labels = [str(item).strip() for item in value]
66
+ return [float(index) for index in range(count)], labels
67
+
68
+
69
+ def _normalize_chart_type(spec: dict[str, Any]) -> str:
70
+ raw = str(spec.get("type") or spec.get("chart_type") or "").strip().lower()
71
+ if raw == "hist":
72
+ raw = "histogram"
73
+ if raw not in _SUPPORTED_CHART_TYPES:
74
+ raise ValueError(
75
+ "Unsupported chart type. Supported types: "
76
+ + ", ".join(sorted(_SUPPORTED_CHART_TYPES))
77
+ + "."
78
+ )
79
+ return raw
80
+
81
+
82
+ def _normalize_series(spec: dict[str, Any]) -> list[dict[str, Any]]:
83
+ raw_series = spec.get("series")
84
+ if isinstance(raw_series, list) and raw_series:
85
+ normalized: list[dict[str, Any]] = []
86
+ for index, entry in enumerate(raw_series):
87
+ if not isinstance(entry, dict):
88
+ continue
89
+ raw_y = entry.get("y", entry.get("values"))
90
+ if raw_y is None:
91
+ continue
92
+ y_values = _coerce_numeric_list(raw_y, f"series[{index}].y")
93
+ x_raw = entry.get("x", entry.get("labels", spec.get("x", spec.get("labels"))))
94
+ x_values, x_labels = _coerce_x_axis(x_raw, len(y_values), f"series[{index}].x")
95
+ name = str(entry.get("name", entry.get("label", f"Series {index + 1}"))).strip()
96
+ color = str(entry.get("color", "")).strip()
97
+ normalized.append(
98
+ {
99
+ "name": name or f"Series {index + 1}",
100
+ "x": x_values,
101
+ "x_labels": x_labels,
102
+ "y": y_values,
103
+ "color": color or None,
104
+ }
105
+ )
106
+ if normalized:
107
+ return normalized
108
+
109
+ raw_y = spec.get("y", spec.get("values"))
110
+ if raw_y is None:
111
+ raise ValueError("chart spec must include y/values or series.")
112
+ y_values = _coerce_numeric_list(raw_y, "y")
113
+ x_values, x_labels = _coerce_x_axis(spec.get("x", spec.get("labels")), len(y_values), "x")
114
+ series_name = str(spec.get("series_name", "Series 1")).strip() or "Series 1"
115
+ color = str(spec.get("color", "")).strip()
116
+ return [
117
+ {
118
+ "name": series_name,
119
+ "x": x_values,
120
+ "x_labels": x_labels,
121
+ "y": y_values,
122
+ "color": color or None,
123
+ }
124
+ ]
125
+
126
+
127
+ def _build_figure(spec: dict[str, Any]) -> tuple[Any, str]:
128
+ plt = _load_matplotlib()
129
+ chart_type = _normalize_chart_type(spec)
130
+ width = _coerce_float(spec.get("width", 8), "width")
131
+ height = _coerce_float(spec.get("height", 4.8), "height")
132
+ fig, ax = plt.subplots(figsize=(max(3.5, min(width, 18.0)), max(2.5, min(height, 12.0))))
133
+ series = _normalize_series(spec) if chart_type in {"line", "bar", "scatter", "area"} else []
134
+
135
+ if chart_type in {"line", "scatter", "area"}:
136
+ for index, entry in enumerate(series):
137
+ name = str(entry.get("name", f"Series {index + 1}")).strip() or f"Series {index + 1}"
138
+ color = entry.get("color")
139
+ x_values = entry.get("x", [])
140
+ y_values = entry.get("y", [])
141
+ x_labels = entry.get("x_labels")
142
+ if chart_type == "line":
143
+ ax.plot(x_values, y_values, label=name, color=color)
144
+ elif chart_type == "scatter":
145
+ ax.scatter(x_values, y_values, label=name, color=color)
146
+ else:
147
+ ax.plot(x_values, y_values, label=name, color=color)
148
+ ax.fill_between(x_values, y_values, alpha=0.3, color=color)
149
+ if isinstance(x_labels, list) and x_labels:
150
+ ax.set_xticks(x_values)
151
+ ax.set_xticklabels(x_labels, rotation=25, ha="right")
152
+
153
+ elif chart_type == "bar":
154
+ stacked = bool(spec.get("stacked", False))
155
+ if len(series) == 1:
156
+ entry = series[0]
157
+ x_values = entry.get("x", [])
158
+ y_values = entry.get("y", [])
159
+ ax.bar(x_values, y_values, color=entry.get("color"), label=str(entry.get("name", "")).strip() or None)
160
+ x_labels = entry.get("x_labels")
161
+ if isinstance(x_labels, list) and x_labels:
162
+ ax.set_xticks(x_values)
163
+ ax.set_xticklabels(x_labels, rotation=25, ha="right")
164
+ else:
165
+ max_count = max(len(entry.get("y", [])) for entry in series)
166
+ explicit_labels = spec.get("labels", spec.get("x"))
167
+ if isinstance(explicit_labels, list) and len(explicit_labels) == max_count:
168
+ category_labels = [str(item).strip() for item in explicit_labels]
169
+ else:
170
+ first_labels = series[0].get("x_labels")
171
+ if isinstance(first_labels, list) and len(first_labels) == max_count:
172
+ category_labels = [str(item).strip() for item in first_labels]
173
+ else:
174
+ category_labels = [str(index + 1) for index in range(max_count)]
175
+ positions = [float(index) for index in range(max_count)]
176
+ if stacked:
177
+ bottoms = [0.0 for _ in range(max_count)]
178
+ for index, entry in enumerate(series):
179
+ y_raw = list(entry.get("y", []))
180
+ y_values = y_raw + [0.0 for _ in range(max_count - len(y_raw))]
181
+ ax.bar(
182
+ positions,
183
+ y_values,
184
+ bottom=bottoms,
185
+ label=str(entry.get("name", f"Series {index + 1}")),
186
+ color=entry.get("color"),
187
+ )
188
+ bottoms = [bottoms[i] + y_values[i] for i in range(max_count)]
189
+ else:
190
+ width_each = 0.8 / max(1, len(series))
191
+ for index, entry in enumerate(series):
192
+ y_raw = list(entry.get("y", []))
193
+ y_values = y_raw + [0.0 for _ in range(max_count - len(y_raw))]
194
+ offsets = [
195
+ position - 0.4 + (width_each / 2.0) + (index * width_each)
196
+ for position in positions
197
+ ]
198
+ ax.bar(
199
+ offsets,
200
+ y_values,
201
+ width=width_each,
202
+ label=str(entry.get("name", f"Series {index + 1}")),
203
+ color=entry.get("color"),
204
+ )
205
+ ax.set_xticks(positions)
206
+ ax.set_xticklabels(category_labels, rotation=25, ha="right")
207
+
208
+ elif chart_type == "pie":
209
+ raw_values = spec.get("values", spec.get("y"))
210
+ values = _coerce_numeric_list(raw_values, "values")
211
+ raw_labels = spec.get("labels", spec.get("x"))
212
+ pie_labels: list[str] | None = None
213
+ if isinstance(raw_labels, list) and len(raw_labels) == len(values):
214
+ pie_labels = [str(item).strip() for item in raw_labels]
215
+ colors = spec.get("colors")
216
+ if not isinstance(colors, list):
217
+ colors = None
218
+ autopct = str(spec.get("autopct", "%1.1f%%")).strip()
219
+ show_pct = bool(spec.get("show_percentages", True))
220
+ wedgeprops = {"width": 0.45} if bool(spec.get("donut", False)) else None
221
+ ax.pie(
222
+ values,
223
+ labels=pie_labels,
224
+ colors=colors,
225
+ autopct=autopct if show_pct else None,
226
+ startangle=90,
227
+ wedgeprops=wedgeprops,
228
+ )
229
+ ax.axis("equal")
230
+
231
+ elif chart_type == "histogram":
232
+ bins = int(_coerce_float(spec.get("bins", 10), "bins"))
233
+ bins = max(2, min(200, bins))
234
+ color = str(spec.get("color", "")).strip() or None
235
+ raw_series = spec.get("series")
236
+ if isinstance(raw_series, list) and raw_series:
237
+ datasets: list[list[float]] = []
238
+ series_labels: list[str] = []
239
+ for index, entry in enumerate(raw_series):
240
+ if not isinstance(entry, dict):
241
+ continue
242
+ raw_values = entry.get("values", entry.get("y"))
243
+ if raw_values is None:
244
+ continue
245
+ datasets.append(_coerce_numeric_list(raw_values, f"series[{index}].values"))
246
+ series_labels.append(str(entry.get("name", entry.get("label", f"Series {index + 1}"))).strip() or f"Series {index + 1}")
247
+ if not datasets:
248
+ raise ValueError("histogram series must include values.")
249
+ ax.hist(
250
+ datasets,
251
+ bins=bins,
252
+ stacked=bool(spec.get("stacked", False)),
253
+ alpha=0.65,
254
+ label=series_labels if len(series_labels) > 1 else None,
255
+ )
256
+ else:
257
+ values = _coerce_numeric_list(spec.get("values", spec.get("y")), "values")
258
+ ax.hist(values, bins=bins, alpha=0.75, color=color)
259
+
260
+ title = str(spec.get("title", "")).strip()
261
+ x_label = str(spec.get("x_label", spec.get("xlabel", ""))).strip()
262
+ y_label = str(spec.get("y_label", spec.get("ylabel", ""))).strip()
263
+ if title:
264
+ ax.set_title(title)
265
+ if x_label:
266
+ ax.set_xlabel(x_label)
267
+ if y_label:
268
+ ax.set_ylabel(y_label)
269
+ if bool(spec.get("grid", chart_type in {"line", "bar", "scatter", "area", "histogram"})):
270
+ ax.grid(True, alpha=0.25)
271
+ if chart_type != "pie" and bool(spec.get("legend", len(series) > 1)):
272
+ handles, labels = ax.get_legend_handles_labels()
273
+ if handles and labels:
274
+ ax.legend()
275
+
276
+ fig.tight_layout()
277
+ return fig, chart_type
278
+
279
+
280
+ def _serialize_figure(fig: Any, image_format: str, dpi: int) -> tuple[str, str]:
281
+ clean_format = str(image_format or "png").strip().lower()
282
+ if clean_format not in {"png", "svg"}:
283
+ raise ValueError("image_format must be 'png' or 'svg'.")
284
+
285
+ buffer = BytesIO()
286
+ if clean_format == "png":
287
+ fig.savefig(buffer, format="png", bbox_inches="tight", dpi=max(60, min(int(dpi), 240)))
288
+ mime_type = "image/png"
289
+ else:
290
+ fig.savefig(buffer, format="svg", bbox_inches="tight")
291
+ mime_type = "image/svg+xml"
292
+ buffer.seek(0)
293
+ encoded = base64.b64encode(buffer.read()).decode("utf-8")
294
+ return encoded, mime_type
295
+
296
+
297
+ def create_chart_artifacts(
298
+ chart_spec: dict[str, Any],
299
+ *,
300
+ title: str = "",
301
+ image_format: str = "png",
302
+ dpi: int = 120,
303
+ ) -> dict[str, Any]:
304
+ """Render a chart from a JSON-style spec and return reusable assets."""
305
+ if not isinstance(chart_spec, dict):
306
+ raise ValueError("chart_spec must be a JSON object.")
307
+
308
+ plt = _load_matplotlib()
309
+ fig, chart_type = _build_figure(chart_spec)
310
+ image_base64, mime_type = _serialize_figure(fig, image_format, dpi)
311
+ plt.close(fig)
312
+
313
+ resolved_title = str(title or chart_spec.get("title", "")).strip() or "Chart"
314
+ alt = str(chart_spec.get("alt", resolved_title)).strip() or "Chart"
315
+ caption = str(chart_spec.get("caption", "")).strip()
316
+ data_uri = f"data:{mime_type};base64,{image_base64}"
317
+ safe_alt = html.escape(alt)
318
+ safe_caption = html.escape(caption)
319
+ safe_title = html.escape(resolved_title)
320
+ html_img_tag = (
321
+ f'<img src="{data_uri}" alt="{safe_alt}" '
322
+ 'style="max-width:100%;height:auto;border-radius:10px;border:1px solid rgba(148,163,184,0.35);" />'
323
+ )
324
+ html_document = (
325
+ "<!doctype html><html><head><meta charset=\"utf-8\" />"
326
+ f"<title>{safe_title}</title>"
327
+ "</head><body style=\"margin:0;padding:20px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;\">"
328
+ f"<h2 style=\"margin:0 0 12px;\">{safe_title}</h2>"
329
+ f"<figure style=\"margin:0;\">{html_img_tag}"
330
+ + (f"<figcaption style=\"margin-top:8px;color:#64748b;\">{safe_caption}</figcaption>" if caption else "")
331
+ + "</figure></body></html>"
332
+ )
333
+ canvas_chart_payload = {
334
+ "type": chart_type,
335
+ "title": resolved_title,
336
+ "alt": alt,
337
+ "caption": caption,
338
+ "data_uri": data_uri,
339
+ }
340
+ canvas_chart_block = "```canvas-chart\n" + json.dumps(
341
+ canvas_chart_payload,
342
+ ensure_ascii=False,
343
+ ) + "\n```"
344
+ canvas_card_item = {
345
+ "type": "image",
346
+ "label": resolved_title,
347
+ "url": data_uri,
348
+ "alt": alt,
349
+ "caption": caption,
350
+ }
351
+
352
+ return {
353
+ "title": resolved_title,
354
+ "chart_type": chart_type,
355
+ "image_format": "svg" if mime_type == "image/svg+xml" else "png",
356
+ "mime_type": mime_type,
357
+ "image_base64": image_base64,
358
+ "data_uri": data_uri,
359
+ "html_img_tag": html_img_tag,
360
+ "html_document": html_document,
361
+ "canvas_chart_payload": canvas_chart_payload,
362
+ "canvas_chart_block": canvas_chart_block,
363
+ "canvas_card_item": canvas_card_item,
364
+ }
@@ -0,0 +1,132 @@
1
+ """HTML report generation utilities.
2
+
3
+ This module provides utilities for generating HTML reports from templates
4
+ and exporting data in multiple formats.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import pandas as pd
13
+ from jinja2 import Environment, FileSystemLoader
14
+ from pydantic import BaseModel
15
+
16
+
17
+ class HTMLReportGenerator:
18
+ """Generate HTML reports from Jinja2 templates.
19
+
20
+ Example:
21
+ ```python
22
+ from pathlib import Path
23
+ from ellements.reporting import HTMLReportGenerator
24
+
25
+ generator = HTMLReportGenerator(template_dir=Path("templates"))
26
+ html = generator.generate(
27
+ template_name="report.html",
28
+ context={"title": "My Report", "data": [1, 2, 3]},
29
+ output_path=Path("report.html"),
30
+ )
31
+
32
+ generator = HTMLReportGenerator(template_string="<h1>{{title}}</h1>")
33
+ html = generator.generate(context={"title": "Hello"})
34
+ ```
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ template_dir: Path | None = None,
40
+ template_string: str | None = None,
41
+ ) -> None:
42
+ """Initialize the HTML report generator.
43
+
44
+ Args:
45
+ template_dir: Directory containing Jinja2 templates.
46
+ template_string: Template string (alternative to *template_dir*).
47
+
48
+ Raises:
49
+ ValueError: If neither *template_dir* nor *template_string* is
50
+ provided.
51
+ """
52
+ if template_dir:
53
+ self.env = Environment(
54
+ loader=FileSystemLoader(str(template_dir)),
55
+ autoescape=False,
56
+ )
57
+ self.template_dir: Path | None = template_dir
58
+ self.template_string: str | None = None
59
+ elif template_string:
60
+ self.env = Environment(autoescape=False)
61
+ self.template_string = template_string
62
+ self.template_dir = None
63
+ else:
64
+ raise ValueError(
65
+ "Either template_dir or template_string must be provided"
66
+ )
67
+
68
+ def generate(
69
+ self,
70
+ template_name: str | None = None,
71
+ context: dict[str, Any] | None = None,
72
+ output_path: Path | None = None,
73
+ ) -> str:
74
+ """Generate an HTML report.
75
+
76
+ Args:
77
+ template_name: Name of the template file (required when
78
+ ``template_dir`` is used).
79
+ context: Dictionary of template variables.
80
+ output_path: Optional path to write the rendered HTML.
81
+
82
+ Returns:
83
+ The rendered HTML as a string.
84
+ """
85
+ context = context or {}
86
+ if self.template_dir and template_name:
87
+ template = self.env.get_template(template_name)
88
+ elif self.template_string:
89
+ template = self.env.from_string(self.template_string)
90
+ else:
91
+ raise ValueError(
92
+ "template_name required when using template_dir, "
93
+ "or use template_string in __init__"
94
+ )
95
+
96
+ html = str(template.render(**context))
97
+
98
+ if output_path:
99
+ output_path.write_text(html, encoding="utf-8")
100
+
101
+ return html
102
+
103
+
104
+ class MultiFormatReportExporter:
105
+ """Export reports in multiple formats (HTML, JSON, CSV)."""
106
+
107
+ def __init__(self, output_dir: Path) -> None:
108
+ """Initialize the multi-format exporter.
109
+
110
+ Args:
111
+ output_dir: Directory to write output files (created if needed).
112
+ """
113
+ self.output_dir = output_dir
114
+ self.output_dir.mkdir(parents=True, exist_ok=True)
115
+
116
+ def export_html(self, content: str, filename: str = "report.html") -> Path:
117
+ """Write *content* as an HTML file under :attr:`output_dir`."""
118
+ path = self.output_dir / filename
119
+ path.write_text(content, encoding="utf-8")
120
+ return path
121
+
122
+ def export_json(self, data: BaseModel, filename: str = "report.json") -> Path:
123
+ """Export a Pydantic model as JSON."""
124
+ path = self.output_dir / filename
125
+ path.write_text(data.model_dump_json(indent=2), encoding="utf-8")
126
+ return path
127
+
128
+ def export_csv(self, df: pd.DataFrame, filename: str = "data.csv") -> Path:
129
+ """Export a DataFrame as CSV."""
130
+ path = self.output_dir / filename
131
+ df.to_csv(path, index=False)
132
+ return path
File without changes
@@ -0,0 +1,73 @@
1
+ """Visualization utilities for embedding charts in reports.
2
+
3
+ This module provides utilities for converting matplotlib figures to embeddable
4
+ formats (base64 PNG/SVG) for use in HTML reports.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ from io import BytesIO
11
+
12
+ import matplotlib.pyplot as plt
13
+
14
+
15
+ class ChartSerializer:
16
+ """Convert matplotlib charts to embeddable formats."""
17
+
18
+ @staticmethod
19
+ def fig_to_base64_png(
20
+ fig: plt.Figure, dpi: int = 100, close_fig: bool = True
21
+ ) -> str:
22
+ """Convert a matplotlib figure to a base64-encoded PNG."""
23
+ buffer = BytesIO()
24
+ fig.savefig(buffer, format="png", bbox_inches="tight", dpi=dpi)
25
+ buffer.seek(0)
26
+ img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
27
+
28
+ if close_fig:
29
+ plt.close(fig)
30
+
31
+ return img_base64
32
+
33
+ @staticmethod
34
+ def fig_to_base64_svg(fig: plt.Figure, close_fig: bool = True) -> str:
35
+ """Convert a matplotlib figure to a base64-encoded SVG."""
36
+ buffer = BytesIO()
37
+ fig.savefig(buffer, format="svg", bbox_inches="tight")
38
+ buffer.seek(0)
39
+ svg_base64 = base64.b64encode(buffer.read()).decode("utf-8")
40
+
41
+ if close_fig:
42
+ plt.close(fig)
43
+
44
+ return svg_base64
45
+
46
+ @staticmethod
47
+ def fig_to_html_img_tag(
48
+ fig: plt.Figure,
49
+ image_format: str = "png",
50
+ alt: str = "Chart",
51
+ dpi: int = 100,
52
+ close_fig: bool = True,
53
+ ) -> str:
54
+ """Convert a figure directly to an HTML ``<img>`` tag.
55
+
56
+ Args:
57
+ fig: Matplotlib figure.
58
+ image_format: Either ``"png"`` or ``"svg"``.
59
+ alt: Alt text for the image.
60
+ dpi: DPI for PNG output (ignored for SVG).
61
+ close_fig: Whether to close the figure after conversion.
62
+ """
63
+ if image_format == "png":
64
+ data = ChartSerializer.fig_to_base64_png(
65
+ fig, dpi=dpi, close_fig=close_fig
66
+ )
67
+ return f'<img src="data:image/png;base64,{data}" alt="{alt}">'
68
+ if image_format == "svg":
69
+ data = ChartSerializer.fig_to_base64_svg(fig, close_fig=close_fig)
70
+ return f'<img src="data:image/svg+xml;base64,{data}" alt="{alt}">'
71
+ raise ValueError(
72
+ f"Unsupported image_format: {image_format}. Use 'png' or 'svg'."
73
+ )
@@ -0,0 +1,22 @@
1
+ """Framework-agnostic standard tools for LLM applications.
2
+
3
+ The top-level package exposes dependency-light terminal tools. Web search,
4
+ crawling, and YouTube tools live under :mod:`ellements.standard_tools.web` and
5
+ require the relevant optional extras.
6
+ """
7
+
8
+ from .terminal import (
9
+ TerminalExecutionResult,
10
+ TerminalExecutionSpec,
11
+ run_terminal_execution,
12
+ terminal_cli_tool,
13
+ terminal_command_text,
14
+ )
15
+
16
+ __all__ = [
17
+ "TerminalExecutionResult",
18
+ "TerminalExecutionSpec",
19
+ "run_terminal_execution",
20
+ "terminal_cli_tool",
21
+ "terminal_command_text",
22
+ ]
File without changes