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,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
|