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,551 @@
|
|
|
1
|
+
"""Rich renderers for human-facing FSLM output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from rich import box
|
|
13
|
+
from rich.align import Align
|
|
14
|
+
from rich.console import Console, Group, RenderableType
|
|
15
|
+
from rich.json import JSON
|
|
16
|
+
from rich.padding import Padding
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich.theme import Theme
|
|
21
|
+
|
|
22
|
+
from .definition import MachineDefinition
|
|
23
|
+
from .models import FSLMEvent, MachineSnapshot, MachineSpec, StepResult
|
|
24
|
+
|
|
25
|
+
ColorMode = Literal["auto", "always", "never"]
|
|
26
|
+
|
|
27
|
+
THEME = Theme(
|
|
28
|
+
{
|
|
29
|
+
"brand": "bold #6C63FF",
|
|
30
|
+
"brand.dim": "#4A42B0",
|
|
31
|
+
"accent": "bold #56D6C1",
|
|
32
|
+
"accent.dim": "#3BA899",
|
|
33
|
+
"amber": "bold #FFB347",
|
|
34
|
+
"amber.dim": "#CC8F39",
|
|
35
|
+
"danger": "bold #FF6B6B",
|
|
36
|
+
"muted": "#A8A4CE",
|
|
37
|
+
"success": "bold #56D6C1",
|
|
38
|
+
"state": "bold #56D6C1",
|
|
39
|
+
"transition": "bold #FFB347",
|
|
40
|
+
"blocked": "bold #FF6B6B",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def make_console(color: ColorMode = "auto") -> Console:
|
|
46
|
+
"""Create the standard FSLM Rich console."""
|
|
47
|
+
|
|
48
|
+
return Console(
|
|
49
|
+
theme=THEME,
|
|
50
|
+
force_terminal=True if color == "always" else None,
|
|
51
|
+
no_color=color == "never",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def render_validation(
|
|
56
|
+
spec: MachineSpec,
|
|
57
|
+
*,
|
|
58
|
+
console: Console | None = None,
|
|
59
|
+
color: ColorMode = "auto",
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Render a successful machine validation."""
|
|
62
|
+
|
|
63
|
+
out = console or make_console(color)
|
|
64
|
+
_app_header(
|
|
65
|
+
"FSLM",
|
|
66
|
+
{"Machine": spec.name, "States": str(len(spec.states)), "Transitions": str(len(spec.transitions))},
|
|
67
|
+
icon="◆",
|
|
68
|
+
border_style="brand",
|
|
69
|
+
console=out,
|
|
70
|
+
)
|
|
71
|
+
out.print(
|
|
72
|
+
Panel(
|
|
73
|
+
"[success]✓ Machine definition is valid[/]\n"
|
|
74
|
+
"[muted]The graph, declared states, transitions, and runtime policy loaded successfully.[/]",
|
|
75
|
+
title="[success]Ready[/]",
|
|
76
|
+
title_align="left",
|
|
77
|
+
border_style="accent.dim",
|
|
78
|
+
box=box.ROUNDED,
|
|
79
|
+
padding=(1, 2),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def render_snapshot(
|
|
85
|
+
snapshot: MachineSnapshot,
|
|
86
|
+
*,
|
|
87
|
+
spec: MachineSpec | None = None,
|
|
88
|
+
title: str = "FSLM Snapshot",
|
|
89
|
+
console: Console | None = None,
|
|
90
|
+
color: ColorMode = "auto",
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Render a snapshot in a compact, human-readable layout."""
|
|
93
|
+
|
|
94
|
+
out = console or make_console(color)
|
|
95
|
+
_app_header(
|
|
96
|
+
title,
|
|
97
|
+
{
|
|
98
|
+
"Machine": snapshot.machine_name,
|
|
99
|
+
"Instance": snapshot.machine_id,
|
|
100
|
+
"Step": str(snapshot.step_index),
|
|
101
|
+
},
|
|
102
|
+
icon="◆",
|
|
103
|
+
border_style="brand",
|
|
104
|
+
console=out,
|
|
105
|
+
)
|
|
106
|
+
out.print(
|
|
107
|
+
Panel(
|
|
108
|
+
_state_card(
|
|
109
|
+
snapshot.current_state,
|
|
110
|
+
spec,
|
|
111
|
+
label="Current state",
|
|
112
|
+
border_style="accent.dim",
|
|
113
|
+
),
|
|
114
|
+
border_style="brand.dim",
|
|
115
|
+
box=box.ROUNDED,
|
|
116
|
+
padding=(1, 2),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
if snapshot.variables:
|
|
120
|
+
out.print(_mapping_panel("Snapshot variables", snapshot.variables, border_style="brand.dim"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def render_step(
|
|
124
|
+
result: StepResult,
|
|
125
|
+
*,
|
|
126
|
+
definition: MachineDefinition | None = None,
|
|
127
|
+
event: FSLMEvent | None = None,
|
|
128
|
+
state_dir: Path | None = None,
|
|
129
|
+
console: Console | None = None,
|
|
130
|
+
color: ColorMode = "auto",
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Render one machine step as a polished state-transition view."""
|
|
133
|
+
|
|
134
|
+
out = console or make_console(color)
|
|
135
|
+
spec = definition.spec if definition is not None else None
|
|
136
|
+
metadata = {
|
|
137
|
+
"Machine": result.new_snapshot.machine_name,
|
|
138
|
+
"Instance": result.new_snapshot.machine_id,
|
|
139
|
+
"Step": str(result.new_snapshot.step_index),
|
|
140
|
+
"Event": event.type if event is not None else result.event_id[:8],
|
|
141
|
+
}
|
|
142
|
+
_app_header(
|
|
143
|
+
"FSLM State Movement",
|
|
144
|
+
metadata,
|
|
145
|
+
icon="◆",
|
|
146
|
+
border_style=_status_border(result.status),
|
|
147
|
+
console=out,
|
|
148
|
+
)
|
|
149
|
+
out.print(_movement_panel(result, spec))
|
|
150
|
+
out.print(_decision_panel(result))
|
|
151
|
+
_render_outputs(out, result)
|
|
152
|
+
_render_actions(out, result)
|
|
153
|
+
if result.violations:
|
|
154
|
+
out.print(_list_panel("Violations", result.violations, border_style="danger"))
|
|
155
|
+
out.print(_trace_panel(result, event))
|
|
156
|
+
footer: dict[str, str | int | float] = {
|
|
157
|
+
"Status": result.status,
|
|
158
|
+
"Transition": result.selected_transition or "none",
|
|
159
|
+
"State": result.target_state,
|
|
160
|
+
}
|
|
161
|
+
if state_dir is not None and not state_dir.name.startswith("tmp."):
|
|
162
|
+
footer["Saved"] = _short_path(state_dir)
|
|
163
|
+
_stats_footer(footer, border_style="brand.dim", console=out)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _movement_panel(result: StepResult, spec: MachineSpec | None) -> Panel:
|
|
167
|
+
grid = Table.grid(expand=True)
|
|
168
|
+
grid.add_column(ratio=5)
|
|
169
|
+
grid.add_column(ratio=3)
|
|
170
|
+
grid.add_column(ratio=5)
|
|
171
|
+
grid.add_row(
|
|
172
|
+
_state_card(result.source_state, spec, label="From", border_style="brand.dim"),
|
|
173
|
+
_transition_card(result),
|
|
174
|
+
_state_card(result.target_state, spec, label="To", border_style=_status_border(result.status)),
|
|
175
|
+
)
|
|
176
|
+
return Panel(
|
|
177
|
+
grid,
|
|
178
|
+
title="[bold white]Machine movement[/]",
|
|
179
|
+
title_align="left",
|
|
180
|
+
border_style=_status_border(result.status),
|
|
181
|
+
box=box.DOUBLE,
|
|
182
|
+
padding=(1, 2),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _state_card(
|
|
187
|
+
state_name: str,
|
|
188
|
+
spec: MachineSpec | None,
|
|
189
|
+
*,
|
|
190
|
+
label: str,
|
|
191
|
+
border_style: str,
|
|
192
|
+
) -> Panel:
|
|
193
|
+
title = Text(label, style="muted")
|
|
194
|
+
state = spec.states.get(state_name) if spec is not None else None
|
|
195
|
+
lines: list[RenderableType] = [
|
|
196
|
+
Text(_humanize(state_name), style="state"),
|
|
197
|
+
Text(state_name, style="muted"),
|
|
198
|
+
]
|
|
199
|
+
objective = state.objective if state is not None else ""
|
|
200
|
+
if objective:
|
|
201
|
+
lines.append(Text(objective, style="white"))
|
|
202
|
+
if state is not None and state.terminal:
|
|
203
|
+
lines.append(Text("terminal state", style="amber"))
|
|
204
|
+
return Panel(
|
|
205
|
+
Group(*lines),
|
|
206
|
+
title=title,
|
|
207
|
+
title_align="left",
|
|
208
|
+
border_style=border_style,
|
|
209
|
+
box=box.ROUNDED,
|
|
210
|
+
padding=(1, 2),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _transition_card(result: StepResult) -> Panel:
|
|
215
|
+
selected = result.selected_transition or "No transition selected"
|
|
216
|
+
body = Table.grid(expand=True)
|
|
217
|
+
body.add_column(justify="center")
|
|
218
|
+
body.add_row(Text("→", style="bold white"))
|
|
219
|
+
body.add_row(Text(_humanize(selected), style="transition"))
|
|
220
|
+
body.add_row(Text(selected, style="muted"))
|
|
221
|
+
body.add_row(_status_badge(result.status))
|
|
222
|
+
return Panel(
|
|
223
|
+
Align.center(body),
|
|
224
|
+
title="[muted]Transition[/]",
|
|
225
|
+
title_align="left",
|
|
226
|
+
border_style=_status_border(result.status),
|
|
227
|
+
box=box.ROUNDED,
|
|
228
|
+
padding=(1, 2),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _decision_panel(result: StepResult) -> Panel:
|
|
233
|
+
if not result.guard_results and not result.invariant_results:
|
|
234
|
+
return Panel(
|
|
235
|
+
"[muted]No guards or invariants were evaluated for this step.[/]",
|
|
236
|
+
title="[bold white]Decision[/]",
|
|
237
|
+
title_align="left",
|
|
238
|
+
border_style="brand.dim",
|
|
239
|
+
box=box.ROUNDED,
|
|
240
|
+
padding=(1, 2),
|
|
241
|
+
)
|
|
242
|
+
table = Table(
|
|
243
|
+
box=box.SIMPLE_HEAVY,
|
|
244
|
+
border_style="brand.dim",
|
|
245
|
+
show_header=True,
|
|
246
|
+
header_style="#A8A4CE",
|
|
247
|
+
expand=True,
|
|
248
|
+
)
|
|
249
|
+
table.add_column("Check", style="white", ratio=3)
|
|
250
|
+
table.add_column("Decision", ratio=2)
|
|
251
|
+
table.add_column("Confidence", justify="right", ratio=1)
|
|
252
|
+
table.add_column("Notes", style="muted", ratio=4)
|
|
253
|
+
for decision in [*result.guard_results, *result.invariant_results]:
|
|
254
|
+
table.add_row(
|
|
255
|
+
decision.id,
|
|
256
|
+
"[success]✓ allowed[/]" if decision.allowed else "[blocked]✗ blocked[/]",
|
|
257
|
+
f"{decision.confidence:.0%}",
|
|
258
|
+
_decision_notes(decision.evidence, decision.uncertainties, decision.alternatives),
|
|
259
|
+
)
|
|
260
|
+
return Panel(
|
|
261
|
+
table,
|
|
262
|
+
title="[bold white]Decision path[/]",
|
|
263
|
+
title_align="left",
|
|
264
|
+
border_style=_status_border(result.status),
|
|
265
|
+
box=box.ROUNDED,
|
|
266
|
+
padding=(1, 2),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _render_outputs(console: Console, result: StepResult) -> None:
|
|
271
|
+
if not result.outputs:
|
|
272
|
+
console.print(
|
|
273
|
+
Panel(
|
|
274
|
+
"[muted]This transition emitted no outputs.[/]",
|
|
275
|
+
title="[bold white]Outputs[/]",
|
|
276
|
+
title_align="left",
|
|
277
|
+
border_style="brand.dim",
|
|
278
|
+
box=box.ROUNDED,
|
|
279
|
+
padding=(1, 2),
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
panels = []
|
|
284
|
+
for output in result.outputs:
|
|
285
|
+
parts: list[RenderableType] = []
|
|
286
|
+
if output.description:
|
|
287
|
+
parts.append(Text(output.description, style="muted"))
|
|
288
|
+
parts.append(_mapping_table(output.payload))
|
|
289
|
+
if output.destination:
|
|
290
|
+
parts.append(Text(f"Destination: {output.destination}", style="accent"))
|
|
291
|
+
panels.append(
|
|
292
|
+
Panel(
|
|
293
|
+
Group(*parts),
|
|
294
|
+
title=f"[accent]{output.type}[/]",
|
|
295
|
+
title_align="left",
|
|
296
|
+
border_style="accent.dim",
|
|
297
|
+
box=box.ROUNDED,
|
|
298
|
+
padding=(1, 2),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
console.print(
|
|
302
|
+
Panel(
|
|
303
|
+
Group(*panels),
|
|
304
|
+
title="[bold white]Emitted outputs[/]",
|
|
305
|
+
title_align="left",
|
|
306
|
+
border_style="accent.dim",
|
|
307
|
+
box=box.ROUNDED,
|
|
308
|
+
padding=(1, 2),
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _render_actions(console: Console, result: StepResult) -> None:
|
|
314
|
+
if not result.actions:
|
|
315
|
+
return
|
|
316
|
+
table = Table(
|
|
317
|
+
box=box.SIMPLE,
|
|
318
|
+
border_style="brand.dim",
|
|
319
|
+
header_style="#A8A4CE",
|
|
320
|
+
expand=True,
|
|
321
|
+
)
|
|
322
|
+
table.add_column("Action")
|
|
323
|
+
table.add_column("Tool")
|
|
324
|
+
table.add_column("Status")
|
|
325
|
+
table.add_column("Message", style="muted")
|
|
326
|
+
for action in result.actions:
|
|
327
|
+
table.add_row(
|
|
328
|
+
action.action_name,
|
|
329
|
+
action.tool,
|
|
330
|
+
_action_status(action.status),
|
|
331
|
+
action.message,
|
|
332
|
+
)
|
|
333
|
+
console.print(
|
|
334
|
+
Panel(
|
|
335
|
+
table,
|
|
336
|
+
title="[bold white]Actions[/]",
|
|
337
|
+
title_align="left",
|
|
338
|
+
border_style="brand.dim",
|
|
339
|
+
box=box.ROUNDED,
|
|
340
|
+
padding=(1, 2),
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _trace_panel(result: StepResult, event: FSLMEvent | None) -> Panel:
|
|
346
|
+
table = Table.grid(expand=True)
|
|
347
|
+
table.add_column(style="muted", no_wrap=True, ratio=1)
|
|
348
|
+
table.add_column(style="white", ratio=4)
|
|
349
|
+
if event is not None:
|
|
350
|
+
table.add_row("Event", f"{event.type} [muted]{event.id[:8]}[/]")
|
|
351
|
+
candidates = result.trace.get("candidate_transitions", [])
|
|
352
|
+
legal = result.trace.get("legal_transitions", [])
|
|
353
|
+
if isinstance(candidates, list):
|
|
354
|
+
table.add_row("Candidates", _join_names(candidates))
|
|
355
|
+
if isinstance(legal, list):
|
|
356
|
+
table.add_row("Legal", _join_names(legal))
|
|
357
|
+
table.add_row("Snapshot", result.new_snapshot.id[:8])
|
|
358
|
+
return Panel(
|
|
359
|
+
table,
|
|
360
|
+
title="[bold white]Trace[/]",
|
|
361
|
+
title_align="left",
|
|
362
|
+
border_style="brand.dim",
|
|
363
|
+
box=box.ROUNDED,
|
|
364
|
+
padding=(1, 2),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _mapping_panel(title: str, mapping: Mapping[str, Any], *, border_style: str) -> Panel:
|
|
369
|
+
return Panel(
|
|
370
|
+
_mapping_table(mapping),
|
|
371
|
+
title=f"[bold white]{title}[/]",
|
|
372
|
+
title_align="left",
|
|
373
|
+
border_style=border_style,
|
|
374
|
+
box=box.ROUNDED,
|
|
375
|
+
padding=(1, 2),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _mapping_table(mapping: Mapping[str, Any]) -> Table:
|
|
380
|
+
table = Table.grid(expand=True)
|
|
381
|
+
table.add_column(style="muted", no_wrap=True, ratio=1)
|
|
382
|
+
table.add_column(style="white", ratio=4)
|
|
383
|
+
for key, value in mapping.items():
|
|
384
|
+
table.add_row(_humanize(str(key)), _value_renderable(value))
|
|
385
|
+
return table
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _list_panel(title: str, values: list[str], *, border_style: str) -> Panel:
|
|
389
|
+
body = "\n".join(f"• {value}" for value in values)
|
|
390
|
+
return Panel(
|
|
391
|
+
body,
|
|
392
|
+
title=f"[bold white]{title}[/]",
|
|
393
|
+
title_align="left",
|
|
394
|
+
border_style=border_style,
|
|
395
|
+
box=box.ROUNDED,
|
|
396
|
+
padding=(1, 2),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _value_renderable(value: Any) -> RenderableType:
|
|
401
|
+
if isinstance(value, str):
|
|
402
|
+
return Text(value)
|
|
403
|
+
if isinstance(value, bool):
|
|
404
|
+
return Text("true" if value else "false", style="success" if value else "danger")
|
|
405
|
+
if isinstance(value, int | float) or value is None:
|
|
406
|
+
return Text(str(value), style="accent")
|
|
407
|
+
summary = _compact_summary(value)
|
|
408
|
+
if summary is not None:
|
|
409
|
+
return Text(summary)
|
|
410
|
+
return Padding(
|
|
411
|
+
JSON(json.dumps(value, ensure_ascii=False)),
|
|
412
|
+
(0, 0, 0, 0),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _compact_summary(value: Any) -> str | None:
|
|
417
|
+
if isinstance(value, Mapping):
|
|
418
|
+
if "news" in value and isinstance(value["news"], Mapping):
|
|
419
|
+
news = value["news"]
|
|
420
|
+
title = str(news.get("title", "news item"))
|
|
421
|
+
tone = news.get("tone")
|
|
422
|
+
severity = news.get("severity")
|
|
423
|
+
parts = [title]
|
|
424
|
+
if tone is not None:
|
|
425
|
+
parts.append(f"tone={tone}")
|
|
426
|
+
if severity is not None:
|
|
427
|
+
parts.append(f"severity={severity}")
|
|
428
|
+
return " · ".join(parts)
|
|
429
|
+
simple: list[str] = []
|
|
430
|
+
for key, item in value.items():
|
|
431
|
+
if len(simple) >= 4:
|
|
432
|
+
simple.append("…")
|
|
433
|
+
break
|
|
434
|
+
if isinstance(item, str | int | float | bool) or item is None:
|
|
435
|
+
simple.append(f"{key}={item}")
|
|
436
|
+
if simple and len(simple) >= len(value):
|
|
437
|
+
return " · ".join(simple)
|
|
438
|
+
if isinstance(value, list) and all(isinstance(item, str) for item in value):
|
|
439
|
+
return " · ".join(value)
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _decision_notes(
|
|
444
|
+
evidence: list[str],
|
|
445
|
+
uncertainties: list[str],
|
|
446
|
+
alternatives: list[str],
|
|
447
|
+
) -> str:
|
|
448
|
+
notes = []
|
|
449
|
+
if evidence:
|
|
450
|
+
notes.append("evidence: " + "; ".join(evidence))
|
|
451
|
+
if uncertainties:
|
|
452
|
+
notes.append("uncertainty: " + "; ".join(uncertainties))
|
|
453
|
+
if alternatives:
|
|
454
|
+
notes.append("alternatives: " + "; ".join(alternatives))
|
|
455
|
+
return " · ".join(notes) if notes else "—"
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _status_badge(status: str) -> Text:
|
|
459
|
+
text = Text()
|
|
460
|
+
if status == "transitioned":
|
|
461
|
+
text.append("✓ transitioned", style="success")
|
|
462
|
+
elif status == "blocked":
|
|
463
|
+
text.append("✗ blocked", style="blocked")
|
|
464
|
+
else:
|
|
465
|
+
text.append("• no transition", style="amber")
|
|
466
|
+
return text
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _action_status(status: str) -> str:
|
|
470
|
+
if status in {"executed", "planned"}:
|
|
471
|
+
return f"[success]{status}[/]"
|
|
472
|
+
if status == "blocked":
|
|
473
|
+
return f"[amber]{status}[/]"
|
|
474
|
+
return f"[danger]{status}[/]"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _status_border(status: str) -> str:
|
|
478
|
+
if status == "transitioned":
|
|
479
|
+
return "accent"
|
|
480
|
+
if status == "blocked":
|
|
481
|
+
return "danger"
|
|
482
|
+
return "amber"
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _join_names(values: list[Any]) -> str:
|
|
486
|
+
if not values:
|
|
487
|
+
return "—"
|
|
488
|
+
return " · ".join(str(value) for value in values)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _humanize(value: str) -> str:
|
|
492
|
+
if not value:
|
|
493
|
+
return "—"
|
|
494
|
+
return value.replace("_", " ").replace("-", " ").strip().capitalize()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _short_path(path: Path) -> str:
|
|
498
|
+
if path.name.startswith("tmp."):
|
|
499
|
+
return path.name
|
|
500
|
+
text = str(path)
|
|
501
|
+
home = str(Path.home())
|
|
502
|
+
if text.startswith(f"{home}/"):
|
|
503
|
+
text = f"~/{text[len(home) + 1:]}"
|
|
504
|
+
if len(text) <= 56:
|
|
505
|
+
return text
|
|
506
|
+
return f"…{text[-55:]}"
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _app_header(
|
|
510
|
+
title: str,
|
|
511
|
+
subtitle_items: Mapping[str, str],
|
|
512
|
+
*,
|
|
513
|
+
icon: str,
|
|
514
|
+
border_style: str,
|
|
515
|
+
console: Console,
|
|
516
|
+
) -> None:
|
|
517
|
+
components = _cli_components()
|
|
518
|
+
components.app_header(
|
|
519
|
+
title,
|
|
520
|
+
dict(subtitle_items),
|
|
521
|
+
icon=icon,
|
|
522
|
+
border_style=border_style,
|
|
523
|
+
console=console,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _stats_footer(
|
|
528
|
+
stats: Mapping[str, str | int | float],
|
|
529
|
+
*,
|
|
530
|
+
border_style: str,
|
|
531
|
+
console: Console,
|
|
532
|
+
) -> None:
|
|
533
|
+
components = _cli_components()
|
|
534
|
+
components.stats_footer(
|
|
535
|
+
dict(stats),
|
|
536
|
+
border_style=border_style,
|
|
537
|
+
console=console,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _cli_components() -> ModuleType:
|
|
542
|
+
return importlib.import_module("ellements.cli.components")
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
__all__ = [
|
|
546
|
+
"ColorMode",
|
|
547
|
+
"make_console",
|
|
548
|
+
"render_snapshot",
|
|
549
|
+
"render_step",
|
|
550
|
+
"render_validation",
|
|
551
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Mermaid rendering for `MachineSpec`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .models import MachineSpec
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_mermaid(spec: MachineSpec) -> str:
|
|
9
|
+
"""Render a machine spec as a Mermaid state diagram."""
|
|
10
|
+
lines = ["stateDiagram-v2", f" [*] --> {spec.initial}"]
|
|
11
|
+
for state in spec.states.values():
|
|
12
|
+
if state.terminal:
|
|
13
|
+
lines.append(f" {state.name} --> [*]")
|
|
14
|
+
for transition in spec.transitions.values():
|
|
15
|
+
label = transition.name
|
|
16
|
+
if transition.weight is not None:
|
|
17
|
+
label = f"{label} ({transition.weight:g})"
|
|
18
|
+
if transition.kind == "recovery":
|
|
19
|
+
label = f"recovery: {label}"
|
|
20
|
+
lines.append(f" {transition.source} --> {transition.target}: {label}")
|
|
21
|
+
return "\n".join(lines) + "\n"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = ["to_mermaid"]
|
|
25
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Reporting, visualization, and export helpers."""
|
|
2
|
+
|
|
3
|
+
from .charts import create_chart_artifacts
|
|
4
|
+
from .html_generation import HTMLReportGenerator, MultiFormatReportExporter
|
|
5
|
+
from .visualization import ChartSerializer
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ChartSerializer",
|
|
9
|
+
"HTMLReportGenerator",
|
|
10
|
+
"MultiFormatReportExporter",
|
|
11
|
+
"create_chart_artifacts",
|
|
12
|
+
]
|