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,411 @@
|
|
|
1
|
+
"""Low-level Rich building blocks for CLI applications.
|
|
2
|
+
|
|
3
|
+
Pure functions with no shared state. Each accepts an optional *console*
|
|
4
|
+
parameter so callers can direct output to stderr (default) or stdout.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from ellements.cli import app_header, key_value_table
|
|
9
|
+
|
|
10
|
+
app_header("My App", {"Model": "gpt-4o"}, icon="🚀")
|
|
11
|
+
key_value_table({"topic": "cats"}, title="Variables")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json as _json
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from rich import box
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
from rich.markdown import Markdown
|
|
23
|
+
from rich.panel import Panel
|
|
24
|
+
from rich.rule import Rule
|
|
25
|
+
from rich.table import Table
|
|
26
|
+
from rich.text import Text
|
|
27
|
+
|
|
28
|
+
# Default console writes to stderr so stdout stays clean for data output
|
|
29
|
+
_default_console = Console(stderr=True)
|
|
30
|
+
|
|
31
|
+
# ── Issue / severity styling ──────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
ISSUE_STYLES: dict[str, tuple[str, str]] = {
|
|
34
|
+
"warning": ("⚠️ ", "yellow"),
|
|
35
|
+
"error": ("❌ ", "red"),
|
|
36
|
+
"suggestion": ("💡 ", "cyan"),
|
|
37
|
+
"info": ("ℹ️ ", "dim"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Building-block functions ──────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
def app_header(
|
|
44
|
+
title: str,
|
|
45
|
+
subtitle_items: dict[str, str] | None = None,
|
|
46
|
+
*,
|
|
47
|
+
icon: str = "",
|
|
48
|
+
border_style: str = "bright_blue",
|
|
49
|
+
console: Console | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Render a styled application header panel.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
title: Application name.
|
|
55
|
+
subtitle_items: Key-value metadata pairs shown below the title
|
|
56
|
+
(e.g. ``{"Model": "gpt-4o", "Spec": "file.md"}``).
|
|
57
|
+
icon: Emoji or symbol prepended to the title.
|
|
58
|
+
border_style: Rich style for the panel border.
|
|
59
|
+
console: Console instance (defaults to stderr).
|
|
60
|
+
"""
|
|
61
|
+
c = console or _default_console
|
|
62
|
+
display_title = f"{icon} {title}".strip() if icon else title
|
|
63
|
+
title_text = Text(display_title, style="bold bright_white")
|
|
64
|
+
|
|
65
|
+
if subtitle_items:
|
|
66
|
+
parts = [
|
|
67
|
+
f"[dim]{k}:[/dim] [bright_cyan]{v}[/]"
|
|
68
|
+
for k, v in subtitle_items.items()
|
|
69
|
+
]
|
|
70
|
+
subtitle = " · ".join(parts)
|
|
71
|
+
else:
|
|
72
|
+
subtitle = ""
|
|
73
|
+
|
|
74
|
+
c.print(Panel(
|
|
75
|
+
subtitle,
|
|
76
|
+
title=title_text,
|
|
77
|
+
title_align="left",
|
|
78
|
+
border_style=border_style,
|
|
79
|
+
box=box.DOUBLE,
|
|
80
|
+
padding=(0, 2),
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def key_value_table(
|
|
85
|
+
data: dict[str, Any],
|
|
86
|
+
*,
|
|
87
|
+
title: str = "",
|
|
88
|
+
name_style: str = "bright_cyan",
|
|
89
|
+
value_style: str = "white",
|
|
90
|
+
console: Console | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Render a two-column key→value table.
|
|
93
|
+
|
|
94
|
+
Booleans are auto-formatted as colored ``true``/``false``.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
data: Mapping of names to values.
|
|
98
|
+
title: Optional table title.
|
|
99
|
+
name_style: Rich style for the name column.
|
|
100
|
+
value_style: Rich style for the value column.
|
|
101
|
+
console: Console instance (defaults to stderr).
|
|
102
|
+
"""
|
|
103
|
+
if not data:
|
|
104
|
+
return
|
|
105
|
+
c = console or _default_console
|
|
106
|
+
|
|
107
|
+
table = Table(
|
|
108
|
+
title=title or None,
|
|
109
|
+
title_style="bold",
|
|
110
|
+
box=box.ROUNDED,
|
|
111
|
+
border_style="dim",
|
|
112
|
+
show_lines=False,
|
|
113
|
+
padding=(0, 1),
|
|
114
|
+
)
|
|
115
|
+
table.add_column("Name", style=name_style, no_wrap=True)
|
|
116
|
+
table.add_column("Value", style=value_style)
|
|
117
|
+
|
|
118
|
+
for key, val in data.items():
|
|
119
|
+
if isinstance(val, bool):
|
|
120
|
+
val_str = "[green]true[/]" if val else "[red]false[/]"
|
|
121
|
+
else:
|
|
122
|
+
val_str = str(val)
|
|
123
|
+
table.add_row(f"[bold]{key}[/]", val_str)
|
|
124
|
+
|
|
125
|
+
c.print(table)
|
|
126
|
+
c.print()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def section_rule(
|
|
130
|
+
title: str,
|
|
131
|
+
*,
|
|
132
|
+
style: str = "bright_green",
|
|
133
|
+
console: Console | None = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Render a horizontal rule with a bold section title."""
|
|
136
|
+
c = console or _default_console
|
|
137
|
+
c.print(Rule(f"[bold]{title}", style=style))
|
|
138
|
+
c.print()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def stats_footer(
|
|
142
|
+
stats: dict[str, str | int | float],
|
|
143
|
+
*,
|
|
144
|
+
border_style: str = "dim",
|
|
145
|
+
console: Console | None = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Render a compact stats panel (key·value pairs on one line)."""
|
|
148
|
+
c = console or _default_console
|
|
149
|
+
parts = [
|
|
150
|
+
f"[dim]{k}:[/] [cyan]{v}[/]"
|
|
151
|
+
for k, v in stats.items()
|
|
152
|
+
]
|
|
153
|
+
c.print(Panel(
|
|
154
|
+
" · ".join(parts),
|
|
155
|
+
border_style=border_style,
|
|
156
|
+
box=box.ROUNDED,
|
|
157
|
+
padding=(0, 1),
|
|
158
|
+
))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def issue_line(
|
|
162
|
+
issue_type: str,
|
|
163
|
+
message: str,
|
|
164
|
+
*,
|
|
165
|
+
console: Console | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Print a single issue line with icon and color."""
|
|
168
|
+
c = console or _default_console
|
|
169
|
+
icon, style = ISSUE_STYLES.get(issue_type, ("ℹ️ ", "dim"))
|
|
170
|
+
c.print(f" {icon}[{style}]{issue_type.upper()}[/]: {message}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def issues_block(
|
|
174
|
+
issues: Sequence[dict[str, str]],
|
|
175
|
+
*,
|
|
176
|
+
title: str = "Issues",
|
|
177
|
+
rule_style: str = "yellow",
|
|
178
|
+
console: Console | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Print a section rule followed by a list of issues."""
|
|
181
|
+
if not issues:
|
|
182
|
+
return
|
|
183
|
+
c = console or _default_console
|
|
184
|
+
section_rule(title, style=rule_style, console=c)
|
|
185
|
+
for issue in issues:
|
|
186
|
+
issue_line(issue.get("type", "info"), issue.get("message", ""), console=c)
|
|
187
|
+
c.print()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def success(message: str, *, console: Console | None = None) -> None:
|
|
191
|
+
"""Print a green success line."""
|
|
192
|
+
(console or _default_console).print(f"[green]✓[/] {message}")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def error(message: str, *, console: Console | None = None) -> None:
|
|
196
|
+
"""Print a red error line."""
|
|
197
|
+
(console or _default_console).print(f"[red]✗[/] {message}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def warning(message: str, *, console: Console | None = None) -> None:
|
|
201
|
+
"""Print a yellow warning line."""
|
|
202
|
+
(console or _default_console).print(f"[yellow]⚠[/] {message}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def step_log(
|
|
206
|
+
message: str,
|
|
207
|
+
*,
|
|
208
|
+
position: str = "mid",
|
|
209
|
+
console: Console | None = None,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Print a tree-style step log line.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
message: The message to display (may contain Rich markup).
|
|
215
|
+
position: ``"first"`` (├─), ``"mid"`` (├─), ``"last"`` (└─),
|
|
216
|
+
or ``"cont"`` (│ ) for continuation lines.
|
|
217
|
+
console: Console instance.
|
|
218
|
+
"""
|
|
219
|
+
c = console or _default_console
|
|
220
|
+
prefixes = {
|
|
221
|
+
"first": "[dim]├─[/]",
|
|
222
|
+
"mid": "[dim]├─[/]",
|
|
223
|
+
"last": "[dim]└─[/]",
|
|
224
|
+
"cont": "[dim]│ [/]",
|
|
225
|
+
}
|
|
226
|
+
prefix = prefixes.get(position, prefixes["mid"])
|
|
227
|
+
c.print(f" {prefix} {message}")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def result_markdown(
|
|
231
|
+
content: str,
|
|
232
|
+
*,
|
|
233
|
+
title: str = "Result",
|
|
234
|
+
rule_style: str = "bright_green",
|
|
235
|
+
console: Console | None = None,
|
|
236
|
+
output_console: Console | None = None,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Print Markdown content preceded by a section rule.
|
|
239
|
+
|
|
240
|
+
The rule goes to *console* (stderr), the content to *output_console*
|
|
241
|
+
(stdout) so piping works correctly.
|
|
242
|
+
"""
|
|
243
|
+
c = console or _default_console
|
|
244
|
+
out = output_console or Console()
|
|
245
|
+
section_rule(title, style=rule_style, console=c)
|
|
246
|
+
out.print(Markdown(content))
|
|
247
|
+
c.print()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def result_json(
|
|
251
|
+
data: Any,
|
|
252
|
+
*,
|
|
253
|
+
output_console: Console | None = None,
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Print JSON data to stdout.
|
|
256
|
+
|
|
257
|
+
Emits the JSON verbatim — wrapping is disabled and Rich markup/emoji
|
|
258
|
+
interpretation is turned off — so downstream consumers (other CLIs,
|
|
259
|
+
GUIs, scripts piping the output) can always parse the result with a
|
|
260
|
+
strict JSON parser. Wrapping would otherwise inject literal newlines
|
|
261
|
+
inside string values when stdout's width is narrower than the longest
|
|
262
|
+
string, breaking JSON conformance per RFC 8259 §7.
|
|
263
|
+
"""
|
|
264
|
+
out = output_console or Console()
|
|
265
|
+
out.print(
|
|
266
|
+
_json.dumps(data, indent=2, ensure_ascii=False),
|
|
267
|
+
highlight=False,
|
|
268
|
+
markup=False,
|
|
269
|
+
emoji=False,
|
|
270
|
+
soft_wrap=True,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ── Chat components ───────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
# Role styles: (label, label_style, border_style, content_style)
|
|
277
|
+
ROLE_STYLES: dict[str, tuple[str, str, str, str]] = {
|
|
278
|
+
"user": ("You", "bold bright_white", "bright_blue", "white"),
|
|
279
|
+
"assistant": ("Assistant", "bold bright_green", "green", ""),
|
|
280
|
+
"system": ("System", "bold yellow", "yellow", "dim"),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def chat_message(
|
|
285
|
+
role: str,
|
|
286
|
+
content: str,
|
|
287
|
+
*,
|
|
288
|
+
label: str | None = None,
|
|
289
|
+
console: Console | None = None,
|
|
290
|
+
output_console: Console | None = None,
|
|
291
|
+
) -> None:
|
|
292
|
+
"""Render a styled chat message.
|
|
293
|
+
|
|
294
|
+
- **user** messages: compact label + text.
|
|
295
|
+
- **assistant** messages: bordered panel with Markdown rendering
|
|
296
|
+
sent to *output_console* (stdout).
|
|
297
|
+
- **system** messages: dimmed panel.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
role: ``"user"``, ``"assistant"``, or ``"system"``.
|
|
301
|
+
content: The message text (Markdown is rendered for assistant).
|
|
302
|
+
label: Override the default role label.
|
|
303
|
+
console: Console for chrome (stderr).
|
|
304
|
+
output_console: Console for assistant content (stdout).
|
|
305
|
+
"""
|
|
306
|
+
c = console or _default_console
|
|
307
|
+
out = output_console or Console()
|
|
308
|
+
style_info = ROLE_STYLES.get(role, ROLE_STYLES["assistant"])
|
|
309
|
+
role_label = label or style_info[0]
|
|
310
|
+
label_style = style_info[1]
|
|
311
|
+
border_style = style_info[2]
|
|
312
|
+
|
|
313
|
+
if role == "user":
|
|
314
|
+
c.print(f"\n[{label_style}]{role_label}[/] [dim]›[/] {content}")
|
|
315
|
+
elif role == "assistant":
|
|
316
|
+
c.print()
|
|
317
|
+
out.print(Panel(
|
|
318
|
+
Markdown(content),
|
|
319
|
+
title=f"[{label_style}]{role_label}[/]",
|
|
320
|
+
title_align="left",
|
|
321
|
+
border_style=border_style,
|
|
322
|
+
box=box.ROUNDED,
|
|
323
|
+
padding=(1, 2),
|
|
324
|
+
))
|
|
325
|
+
else:
|
|
326
|
+
c.print(Panel(
|
|
327
|
+
content,
|
|
328
|
+
title=f"[{label_style}]{role_label}[/]",
|
|
329
|
+
title_align="left",
|
|
330
|
+
border_style=border_style,
|
|
331
|
+
box=box.SIMPLE,
|
|
332
|
+
padding=(0, 2),
|
|
333
|
+
))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def chat_input_prompt(
|
|
337
|
+
label: str = "You",
|
|
338
|
+
*,
|
|
339
|
+
style: str = "bold bright_white",
|
|
340
|
+
) -> str:
|
|
341
|
+
"""Return a styled prompt string for ``console.input()``.
|
|
342
|
+
|
|
343
|
+
Usage::
|
|
344
|
+
|
|
345
|
+
user_input = console.input(chat_input_prompt())
|
|
346
|
+
"""
|
|
347
|
+
return f"\n[{style}]{label}[/] [dim]›[/] "
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def tool_use_badge(
|
|
351
|
+
tool_name: str,
|
|
352
|
+
*,
|
|
353
|
+
console: Console | None = None,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""Print an inline tool-use notification."""
|
|
356
|
+
c = console or _default_console
|
|
357
|
+
c.print(f" [dim]🔧 Using[/] [bright_cyan]{tool_name}[/][dim]…[/]")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def welcome_banner(
|
|
361
|
+
title: str,
|
|
362
|
+
subtitle_items: dict[str, str] | None = None,
|
|
363
|
+
*,
|
|
364
|
+
commands: dict[str, str] | None = None,
|
|
365
|
+
icon: str = "",
|
|
366
|
+
border_style: str = "bright_blue",
|
|
367
|
+
console: Console | None = None,
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Render a welcome banner for interactive CLI apps.
|
|
370
|
+
|
|
371
|
+
Shows the app title, metadata, and optional command hints.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
title: Application name.
|
|
375
|
+
subtitle_items: Key-value pairs (e.g. model, mode).
|
|
376
|
+
commands: Command hints (e.g. ``{"quit": "Exit", "/help": "Help"}``).
|
|
377
|
+
icon: Emoji prepended to title.
|
|
378
|
+
border_style: Panel border style.
|
|
379
|
+
console: Console instance (stderr).
|
|
380
|
+
"""
|
|
381
|
+
c = console or _default_console
|
|
382
|
+
display_title = f"{icon} {title}".strip() if icon else title
|
|
383
|
+
title_text = Text(display_title, style="bold bright_white")
|
|
384
|
+
|
|
385
|
+
lines: list[str] = []
|
|
386
|
+
if subtitle_items:
|
|
387
|
+
parts = [
|
|
388
|
+
f"[dim]{k}:[/] [bright_cyan]{v}[/]"
|
|
389
|
+
for k, v in subtitle_items.items()
|
|
390
|
+
]
|
|
391
|
+
lines.append(" ".join(parts))
|
|
392
|
+
|
|
393
|
+
if commands:
|
|
394
|
+
if lines:
|
|
395
|
+
lines.append("")
|
|
396
|
+
cmd_parts = [
|
|
397
|
+
f" [dim bold]{k}[/] [dim]{v}[/]"
|
|
398
|
+
for k, v in commands.items()
|
|
399
|
+
]
|
|
400
|
+
lines.append("[dim]Commands:[/]")
|
|
401
|
+
lines.extend(cmd_parts)
|
|
402
|
+
|
|
403
|
+
c.print(Panel(
|
|
404
|
+
"\n".join(lines) if lines else "",
|
|
405
|
+
title=title_text,
|
|
406
|
+
title_align="left",
|
|
407
|
+
border_style=border_style,
|
|
408
|
+
box=box.DOUBLE,
|
|
409
|
+
padding=(1, 2),
|
|
410
|
+
))
|
|
411
|
+
c.print()
|
ellements/cli/printer.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""High-level CLI printer with app branding.
|
|
2
|
+
|
|
3
|
+
Wraps the low-level building blocks in :mod:`components` into a stateful,
|
|
4
|
+
branded workflow API that any CLI app can instantiate.
|
|
5
|
+
|
|
6
|
+
All chrome (headers, spinners, events, stats) goes to *stderr*. Only
|
|
7
|
+
actual output (composed prompt, JSON data) goes to *stdout* so pipelines
|
|
8
|
+
stay clean.
|
|
9
|
+
|
|
10
|
+
Verbose progress events are dispatched through a pluggable
|
|
11
|
+
:class:`EventRenderer` Protocol. Apps with their own event vocabulary
|
|
12
|
+
register a renderer at construction time; the printer remains agnostic
|
|
13
|
+
to specific event names.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
from ellements.cli import CliPrinter
|
|
18
|
+
|
|
19
|
+
printer = CliPrinter("Prompt Composer", icon="🧩")
|
|
20
|
+
printer.header({"Spec": "file.md", "Model": "gpt-4o-mini"})
|
|
21
|
+
printer.variables({"topic": "cats"})
|
|
22
|
+
printer.status("Composing prompt…")
|
|
23
|
+
# pass printer.event as an on_event callback
|
|
24
|
+
result = await controller.compose(spec, vars, on_event=printer.event)
|
|
25
|
+
printer.result_markdown(result.composed_prompt)
|
|
26
|
+
printer.stats({"Tool calls": 3, "Issues": 1}, elapsed=2.1)
|
|
27
|
+
printer.done()
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
33
|
+
from contextlib import contextmanager
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, Protocol, runtime_checkable
|
|
36
|
+
|
|
37
|
+
from rich.console import Console
|
|
38
|
+
|
|
39
|
+
from . import components as _c
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class EventRenderer(Protocol):
|
|
44
|
+
"""Renders verbose progress events to the supplied :class:`Console`."""
|
|
45
|
+
|
|
46
|
+
def render(
|
|
47
|
+
self,
|
|
48
|
+
event_type: str,
|
|
49
|
+
data: Mapping[str, Any],
|
|
50
|
+
console: Console,
|
|
51
|
+
) -> None: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CliPrinter:
|
|
55
|
+
"""Branded CLI printer for terminal output.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
app_name: Display name of the application.
|
|
59
|
+
icon: Emoji or symbol prepended to the title.
|
|
60
|
+
border_style: Default Rich border style for panels.
|
|
61
|
+
verbose: When False, :meth:`event` is a no-op.
|
|
62
|
+
event_renderer: Optional plug-in for rendering domain-specific
|
|
63
|
+
verbose events. When None, :meth:`event` accepts events but
|
|
64
|
+
does nothing with them in verbose mode.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
app_name: str,
|
|
70
|
+
*,
|
|
71
|
+
icon: str = "",
|
|
72
|
+
border_style: str = "bright_blue",
|
|
73
|
+
verbose: bool = False,
|
|
74
|
+
event_renderer: EventRenderer | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
self.app_name = app_name
|
|
77
|
+
self.icon = icon
|
|
78
|
+
self.border_style = border_style
|
|
79
|
+
self.verbose = verbose
|
|
80
|
+
self.event_renderer = event_renderer
|
|
81
|
+
self.console = Console(stderr=True)
|
|
82
|
+
self.output_console = Console()
|
|
83
|
+
|
|
84
|
+
# ── Header / preamble ──────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
def header(self, subtitle_items: Mapping[str, str] | None = None) -> None:
|
|
87
|
+
_c.app_header(
|
|
88
|
+
self.app_name,
|
|
89
|
+
dict(subtitle_items) if subtitle_items else None,
|
|
90
|
+
icon=self.icon,
|
|
91
|
+
border_style=self.border_style,
|
|
92
|
+
console=self.console,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def variables(
|
|
96
|
+
self, data: Mapping[str, Any], *, title: str = "📋 Variables"
|
|
97
|
+
) -> None:
|
|
98
|
+
_c.key_value_table(dict(data), title=title, console=self.console)
|
|
99
|
+
|
|
100
|
+
# ── Status / progress ──────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def status(self, message: str) -> None:
|
|
103
|
+
self.console.print()
|
|
104
|
+
self.console.print(
|
|
105
|
+
f"[bold bright_blue]⚙ {message}[/]", highlight=False
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def event(self, event_type: str, data: Mapping[str, Any]) -> None:
|
|
109
|
+
"""Forward an event to the configured renderer, if any.
|
|
110
|
+
|
|
111
|
+
Designed to be passed directly as an ``on_event`` callback. Does
|
|
112
|
+
nothing when *verbose* is False or no renderer is installed.
|
|
113
|
+
"""
|
|
114
|
+
if not self.verbose or self.event_renderer is None:
|
|
115
|
+
return
|
|
116
|
+
self.event_renderer.render(event_type, data, self.console)
|
|
117
|
+
|
|
118
|
+
# ── Results ────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def result_markdown(
|
|
121
|
+
self, content: str, *, title: str = "Result"
|
|
122
|
+
) -> None:
|
|
123
|
+
_c.result_markdown(
|
|
124
|
+
content,
|
|
125
|
+
title=title,
|
|
126
|
+
console=self.console,
|
|
127
|
+
output_console=self.output_console,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def result_json(self, data: Any) -> None:
|
|
131
|
+
_c.result_json(data, output_console=self.output_console)
|
|
132
|
+
|
|
133
|
+
def issues(
|
|
134
|
+
self,
|
|
135
|
+
issues_list: Sequence[Mapping[str, str]],
|
|
136
|
+
*,
|
|
137
|
+
title: str = "Issues",
|
|
138
|
+
) -> None:
|
|
139
|
+
_c.issues_block(
|
|
140
|
+
[dict(i) for i in issues_list],
|
|
141
|
+
title=title,
|
|
142
|
+
console=self.console,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def stats(
|
|
146
|
+
self,
|
|
147
|
+
stats_dict: Mapping[str, str | int | float],
|
|
148
|
+
*,
|
|
149
|
+
elapsed: float | None = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
merged = dict(stats_dict)
|
|
152
|
+
if elapsed is not None:
|
|
153
|
+
merged["Time"] = f"{elapsed:.1f}s"
|
|
154
|
+
_c.stats_footer(merged, console=self.console)
|
|
155
|
+
|
|
156
|
+
def file_written(self, path: str | Path) -> None:
|
|
157
|
+
_c.success(
|
|
158
|
+
f"Output written to [bright_cyan]{path}[/]",
|
|
159
|
+
console=self.console,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def done(self, message: str = "Done. 👋") -> None:
|
|
163
|
+
self.console.print(f"[dim]{message}[/]")
|
|
164
|
+
|
|
165
|
+
# ── Chat ───────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def welcome(
|
|
168
|
+
self,
|
|
169
|
+
subtitle_items: Mapping[str, str] | None = None,
|
|
170
|
+
*,
|
|
171
|
+
commands: Mapping[str, str] | None = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
_c.welcome_banner(
|
|
174
|
+
self.app_name,
|
|
175
|
+
dict(subtitle_items) if subtitle_items else None,
|
|
176
|
+
commands=dict(commands) if commands else None,
|
|
177
|
+
icon=self.icon,
|
|
178
|
+
border_style=self.border_style,
|
|
179
|
+
console=self.console,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def user_message(self, content: str) -> None:
|
|
183
|
+
_c.chat_message(
|
|
184
|
+
"user",
|
|
185
|
+
content,
|
|
186
|
+
console=self.console,
|
|
187
|
+
output_console=self.output_console,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def assistant_message(self, content: str) -> None:
|
|
191
|
+
_c.chat_message(
|
|
192
|
+
"assistant",
|
|
193
|
+
content,
|
|
194
|
+
console=self.console,
|
|
195
|
+
output_console=self.output_console,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def tool_badge(self, tool_name: str) -> None:
|
|
199
|
+
_c.tool_use_badge(tool_name, console=self.console)
|
|
200
|
+
|
|
201
|
+
def input_prompt(self) -> str:
|
|
202
|
+
return _c.chat_input_prompt()
|
|
203
|
+
|
|
204
|
+
@contextmanager
|
|
205
|
+
def thinking(self, message: str = "Thinking…") -> Iterator[None]:
|
|
206
|
+
"""Show a spinner while work is happening."""
|
|
207
|
+
with self.console.status(
|
|
208
|
+
f"[bold bright_blue]{message}[/]",
|
|
209
|
+
spinner="dots",
|
|
210
|
+
spinner_style="bright_blue",
|
|
211
|
+
):
|
|
212
|
+
yield
|
|
213
|
+
|
|
214
|
+
def goodbye(self, message: str = "Goodbye! 👋") -> None:
|
|
215
|
+
self.console.print(f"\n[dim italic]{message}[/]\n")
|
|
216
|
+
|
|
217
|
+
# ── One-liners ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def success(self, message: str) -> None:
|
|
220
|
+
_c.success(message, console=self.console)
|
|
221
|
+
|
|
222
|
+
def error(self, message: str) -> None:
|
|
223
|
+
_c.error(message, console=self.console)
|
|
224
|
+
|
|
225
|
+
def warning(self, message: str) -> None:
|
|
226
|
+
_c.warning(message, console=self.console)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
__all__ = ["CliPrinter", "EventRenderer"]
|
ellements/cli/py.typed
ADDED
|
File without changes
|