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,342 @@
|
|
|
1
|
+
"""BenchmarkModel adapter and orchestration for `lm-evaluation-harness`.
|
|
2
|
+
|
|
3
|
+
Wraps an :class:`~ellements.core.LLMClient` (optionally with a
|
|
4
|
+
:class:`~ellements.execution.Strategy`) as an ``lm_eval.api.model.LM``
|
|
5
|
+
so any prompting strategy can be evaluated against standard benchmarks
|
|
6
|
+
(MMLU, GSM8K, HellaSwag, ARC, HumanEval, …).
|
|
7
|
+
|
|
8
|
+
Key design points:
|
|
9
|
+
|
|
10
|
+
- **Single persistent event loop owned by the harness.** All async LLM
|
|
11
|
+
calls are dispatched onto one background thread running a single
|
|
12
|
+
``asyncio`` loop. No per-call ``asyncio.run`` and no thread-pool
|
|
13
|
+
re-entry. The loop is closed via :meth:`BenchmarkModel.shutdown` or
|
|
14
|
+
by using the model as a context manager.
|
|
15
|
+
- **Provider prefix is preserved.** The full ``"openai/gpt-4o"`` (or
|
|
16
|
+
``"anthropic/..."``, etc.) is passed unchanged to ``LLMClient``;
|
|
17
|
+
litellm needs the prefix for routing.
|
|
18
|
+
- **Hard-fail on missing log-probabilities.** When a task requires
|
|
19
|
+
``loglikelihood`` and the configured model does not return token
|
|
20
|
+
log-probs, the harness raises :class:`LogprobsUnsupportedError`
|
|
21
|
+
immediately rather than silently returning ``0.0``.
|
|
22
|
+
- **Real per-sample progress.** ``on_progress`` fires before each task
|
|
23
|
+
starts (per strategy/task pair) so callers can render a progress bar
|
|
24
|
+
that actually reflects work happening.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import logging
|
|
31
|
+
import threading
|
|
32
|
+
from collections.abc import Callable, Coroutine, Iterable
|
|
33
|
+
from typing import Any, TypeVar
|
|
34
|
+
|
|
35
|
+
from ellements.core import LLMClient, LogprobsUnsupportedError
|
|
36
|
+
from ellements.execution import Strategy, StrategyConfigInput
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from lm_eval.api.model import LM
|
|
40
|
+
except ImportError as exc: # pragma: no cover
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"lm-evaluation-harness is required for benchmarking. "
|
|
43
|
+
"Install it with: pip install 'ellements[benchmarking]'"
|
|
44
|
+
) from exc
|
|
45
|
+
|
|
46
|
+
from .results import BenchmarkComparison
|
|
47
|
+
|
|
48
|
+
_logger = logging.getLogger(__name__)
|
|
49
|
+
T = TypeVar("T")
|
|
50
|
+
|
|
51
|
+
OnBenchmarkProgress = Callable[[str, str, int, int], None]
|
|
52
|
+
"""Callback signature: ``(strategy_name, task_name, current_idx, total)``."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _LoopRunner:
|
|
56
|
+
"""Owns a persistent asyncio event loop on a dedicated thread."""
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self._loop = asyncio.new_event_loop()
|
|
60
|
+
self._thread = threading.Thread(
|
|
61
|
+
target=self._loop.run_forever,
|
|
62
|
+
name="ellements-benchmarking-loop",
|
|
63
|
+
daemon=True,
|
|
64
|
+
)
|
|
65
|
+
self._thread.start()
|
|
66
|
+
|
|
67
|
+
def submit(self, coro: Coroutine[Any, Any, T]) -> T:
|
|
68
|
+
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
69
|
+
return future.result()
|
|
70
|
+
|
|
71
|
+
def close(self) -> None:
|
|
72
|
+
if not self._loop.is_closed():
|
|
73
|
+
self._loop.call_soon_threadsafe(self._loop.stop)
|
|
74
|
+
self._thread.join(timeout=5.0)
|
|
75
|
+
self._loop.close()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BenchmarkModel(LM): # type: ignore[misc]
|
|
79
|
+
"""Wraps an :class:`LLMClient` (optionally with a :class:`Strategy`)
|
|
80
|
+
as an ``lm_eval.api.model.LM``.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
client: The LLM client used for generation.
|
|
84
|
+
strategy: Optional execution strategy. When set, ``generate_until``
|
|
85
|
+
routes through ``strategy.execute()``. Strategies do **not**
|
|
86
|
+
affect ``loglikelihood`` — those are an intrinsic model
|
|
87
|
+
property.
|
|
88
|
+
strategy_config: Extra config forwarded to ``strategy.execute()``.
|
|
89
|
+
requires_logprobs: When True, the harness validates at startup
|
|
90
|
+
that the underlying model returns token log-probabilities,
|
|
91
|
+
raising :class:`LogprobsUnsupportedError` if not.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
client: LLMClient,
|
|
97
|
+
strategy: Strategy | None = None,
|
|
98
|
+
strategy_config: StrategyConfigInput = None,
|
|
99
|
+
*,
|
|
100
|
+
requires_logprobs: bool = False,
|
|
101
|
+
) -> None:
|
|
102
|
+
super().__init__()
|
|
103
|
+
self._client = client
|
|
104
|
+
self._strategy = strategy
|
|
105
|
+
self._strategy_config: StrategyConfigInput = strategy_config or {}
|
|
106
|
+
self._loop = _LoopRunner()
|
|
107
|
+
self._closed = False
|
|
108
|
+
|
|
109
|
+
if requires_logprobs:
|
|
110
|
+
self._verify_logprobs_support()
|
|
111
|
+
|
|
112
|
+
def __enter__(self) -> BenchmarkModel:
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __exit__(self, *_exc: object) -> None:
|
|
116
|
+
self.shutdown()
|
|
117
|
+
|
|
118
|
+
def shutdown(self) -> None:
|
|
119
|
+
"""Shut down the persistent event loop. Idempotent."""
|
|
120
|
+
if not self._closed:
|
|
121
|
+
self._loop.close()
|
|
122
|
+
self._closed = True
|
|
123
|
+
|
|
124
|
+
# ── generate_until (generative benchmarks) ───────────────────────
|
|
125
|
+
|
|
126
|
+
def generate_until(self, requests: list[Any]) -> list[str]:
|
|
127
|
+
results: list[str] = []
|
|
128
|
+
for req in requests:
|
|
129
|
+
context, gen_kwargs = req.args
|
|
130
|
+
stop_seqs: Iterable[str] = gen_kwargs.get("until", []) or []
|
|
131
|
+
|
|
132
|
+
if self._strategy is not None:
|
|
133
|
+
output = self._loop.submit(self._strategy_call(context))
|
|
134
|
+
else:
|
|
135
|
+
output = self._loop.submit(self._direct_call(context))
|
|
136
|
+
|
|
137
|
+
for stop in stop_seqs:
|
|
138
|
+
if stop in output:
|
|
139
|
+
output = output[: output.index(stop)]
|
|
140
|
+
|
|
141
|
+
results.append(output)
|
|
142
|
+
return results
|
|
143
|
+
|
|
144
|
+
async def _direct_call(self, prompt: str) -> str:
|
|
145
|
+
return await self._client.complete(
|
|
146
|
+
[{"role": "user", "content": prompt}]
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def _strategy_call(self, prompt: str) -> str:
|
|
150
|
+
assert self._strategy is not None
|
|
151
|
+
result = await self._strategy.execute(
|
|
152
|
+
prompts={"default": prompt},
|
|
153
|
+
client=self._client,
|
|
154
|
+
tools=None,
|
|
155
|
+
config=self._strategy_config,
|
|
156
|
+
)
|
|
157
|
+
return result.output
|
|
158
|
+
|
|
159
|
+
# ── loglikelihood (multiple-choice benchmarks) ───────────────────
|
|
160
|
+
|
|
161
|
+
def loglikelihood(self, requests: list[Any]) -> list[tuple[float, bool]]:
|
|
162
|
+
"""Compute log-likelihoods. Hard-fails when not supported."""
|
|
163
|
+
results: list[tuple[float, bool]] = []
|
|
164
|
+
for req in requests:
|
|
165
|
+
context, continuation = req.args
|
|
166
|
+
ll, is_greedy = self._loop.submit(
|
|
167
|
+
self._loglikelihood_call(context, continuation)
|
|
168
|
+
)
|
|
169
|
+
results.append((ll, is_greedy))
|
|
170
|
+
return results
|
|
171
|
+
|
|
172
|
+
def loglikelihood_rolling(self, requests: list[Any]) -> list[float]:
|
|
173
|
+
results: list[float] = []
|
|
174
|
+
for req in requests:
|
|
175
|
+
(text,) = req.args
|
|
176
|
+
ll, _ = self._loop.submit(self._loglikelihood_call("", text))
|
|
177
|
+
results.append(ll)
|
|
178
|
+
return results
|
|
179
|
+
|
|
180
|
+
async def _loglikelihood_call(
|
|
181
|
+
self, context: str, continuation: str
|
|
182
|
+
) -> tuple[float, bool]:
|
|
183
|
+
return await self._client.loglikelihood(
|
|
184
|
+
context=context,
|
|
185
|
+
continuation=continuation,
|
|
186
|
+
max_tokens=max(1, len(continuation.split()) * 3),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _verify_logprobs_support(self) -> None:
|
|
190
|
+
"""Make a one-token probe call; raise if logprobs aren't returned."""
|
|
191
|
+
try:
|
|
192
|
+
self._loop.submit(self._loglikelihood_call("Hello", " world"))
|
|
193
|
+
except LogprobsUnsupportedError:
|
|
194
|
+
raise
|
|
195
|
+
except Exception as exc:
|
|
196
|
+
raise LogprobsUnsupportedError(
|
|
197
|
+
f"Probe loglikelihood call failed for model {self._client.model!r}: {exc}"
|
|
198
|
+
) from exc
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── public convenience functions ─────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _tasks_require_logprobs(tasks: list[str]) -> bool:
|
|
205
|
+
"""Best-effort introspection of lm-eval tasks for loglikelihood output type."""
|
|
206
|
+
try:
|
|
207
|
+
from lm_eval import tasks as lm_tasks
|
|
208
|
+
except ImportError: # pragma: no cover
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
task_dict = lm_tasks.get_task_dict(tasks)
|
|
213
|
+
except Exception: # pragma: no cover
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
for value in task_dict.values():
|
|
217
|
+
output_type = getattr(value, "OUTPUT_TYPE", None) or getattr(
|
|
218
|
+
getattr(value, "_config", None), "output_type", None
|
|
219
|
+
)
|
|
220
|
+
if isinstance(output_type, str) and output_type.startswith(
|
|
221
|
+
"loglikelihood"
|
|
222
|
+
):
|
|
223
|
+
return True
|
|
224
|
+
if isinstance(output_type, str) and output_type == "multiple_choice":
|
|
225
|
+
return True
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def run_benchmark(
|
|
230
|
+
*,
|
|
231
|
+
model: str,
|
|
232
|
+
strategy: Strategy | None = None,
|
|
233
|
+
strategy_config: StrategyConfigInput = None,
|
|
234
|
+
tasks: list[str],
|
|
235
|
+
num_fewshot: int | None = None,
|
|
236
|
+
limit: int | float | None = None,
|
|
237
|
+
batch_size: int | None = 1,
|
|
238
|
+
on_progress: OnBenchmarkProgress | None = None,
|
|
239
|
+
strategy_name: str = "default",
|
|
240
|
+
client: LLMClient | None = None,
|
|
241
|
+
**kwargs: Any,
|
|
242
|
+
) -> dict[str, Any]:
|
|
243
|
+
"""Run lm-eval benchmarks, optionally through an ellements strategy.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
model: Litellm-format model id (e.g. ``"openai/gpt-4o"``). The
|
|
247
|
+
full provider prefix is preserved.
|
|
248
|
+
strategy: Optional :class:`Strategy`; routes generative tasks
|
|
249
|
+
through ``strategy.execute()``.
|
|
250
|
+
strategy_config: Extra config forwarded to the strategy.
|
|
251
|
+
tasks: lm-eval task names (required and non-empty).
|
|
252
|
+
num_fewshot: Number of few-shot examples.
|
|
253
|
+
limit: Sample limit per task (int = count; float in [0,1] = ratio).
|
|
254
|
+
batch_size: Batch size for lm-eval (defaults to 1 for API models).
|
|
255
|
+
on_progress: Fires before each task starts.
|
|
256
|
+
strategy_name: Logical label used in progress callbacks.
|
|
257
|
+
client: Optional pre-built :class:`LLMClient`. Useful for sharing
|
|
258
|
+
observers or providing API keys/headers. When None, one is
|
|
259
|
+
constructed from *model*.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
The raw lm-eval results dict.
|
|
263
|
+
"""
|
|
264
|
+
from lm_eval.evaluator import simple_evaluate
|
|
265
|
+
|
|
266
|
+
if not tasks:
|
|
267
|
+
raise ValueError("tasks must be a non-empty list (e.g. ['gsm8k'])")
|
|
268
|
+
|
|
269
|
+
requires_logprobs = _tasks_require_logprobs(tasks)
|
|
270
|
+
|
|
271
|
+
owned_client = client is None
|
|
272
|
+
llm_client = client or LLMClient(model=model)
|
|
273
|
+
|
|
274
|
+
with BenchmarkModel(
|
|
275
|
+
client=llm_client,
|
|
276
|
+
strategy=strategy,
|
|
277
|
+
strategy_config=strategy_config,
|
|
278
|
+
requires_logprobs=requires_logprobs,
|
|
279
|
+
) as benchmark_model:
|
|
280
|
+
for i, task in enumerate(tasks):
|
|
281
|
+
if on_progress is not None:
|
|
282
|
+
on_progress(strategy_name, task, i, len(tasks))
|
|
283
|
+
|
|
284
|
+
results = simple_evaluate(
|
|
285
|
+
model=benchmark_model,
|
|
286
|
+
tasks=tasks,
|
|
287
|
+
num_fewshot=num_fewshot,
|
|
288
|
+
limit=limit,
|
|
289
|
+
batch_size=batch_size,
|
|
290
|
+
**kwargs,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if owned_client:
|
|
294
|
+
_logger.debug("BenchmarkModel finished; owned client released.")
|
|
295
|
+
typed_results: dict[str, Any] = results
|
|
296
|
+
return typed_results
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def compare_benchmarks(
|
|
300
|
+
*,
|
|
301
|
+
model: str,
|
|
302
|
+
strategies: dict[str, Strategy | None],
|
|
303
|
+
tasks: list[str],
|
|
304
|
+
num_fewshot: int | None = None,
|
|
305
|
+
limit: int | float | None = None,
|
|
306
|
+
batch_size: int | None = 1,
|
|
307
|
+
on_progress: OnBenchmarkProgress | None = None,
|
|
308
|
+
client: LLMClient | None = None,
|
|
309
|
+
**kwargs: Any,
|
|
310
|
+
) -> BenchmarkComparison:
|
|
311
|
+
"""Run the same benchmarks across multiple strategies and compare results."""
|
|
312
|
+
if not tasks:
|
|
313
|
+
raise ValueError("tasks must be a non-empty list (e.g. ['gsm8k'])")
|
|
314
|
+
|
|
315
|
+
all_results: dict[str, dict[str, Any]] = {}
|
|
316
|
+
|
|
317
|
+
for name, strategy in strategies.items():
|
|
318
|
+
_logger.info("Running benchmarks for strategy: %s", name)
|
|
319
|
+
all_results[name] = run_benchmark(
|
|
320
|
+
model=model,
|
|
321
|
+
strategy=strategy,
|
|
322
|
+
tasks=tasks,
|
|
323
|
+
num_fewshot=num_fewshot,
|
|
324
|
+
limit=limit,
|
|
325
|
+
batch_size=batch_size,
|
|
326
|
+
on_progress=on_progress,
|
|
327
|
+
strategy_name=name,
|
|
328
|
+
client=client,
|
|
329
|
+
**kwargs,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return BenchmarkComparison(
|
|
333
|
+
model=model, tasks=tasks, results=all_results
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
__all__ = [
|
|
338
|
+
"BenchmarkModel",
|
|
339
|
+
"OnBenchmarkProgress",
|
|
340
|
+
"compare_benchmarks",
|
|
341
|
+
"run_benchmark",
|
|
342
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Benchmark comparison results with formatting and export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BenchmarkComparison:
|
|
12
|
+
"""Holds results from :func:`compare_benchmarks` and provides
|
|
13
|
+
formatting, comparison, and export utilities.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
model : str
|
|
18
|
+
The model used across all strategies.
|
|
19
|
+
tasks : list[str]
|
|
20
|
+
The benchmark tasks that were run.
|
|
21
|
+
results : dict[str, dict]
|
|
22
|
+
Map of strategy name → raw lm-eval results dict.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model: str
|
|
26
|
+
tasks: list[str]
|
|
27
|
+
results: dict[str, dict[str, Any]]
|
|
28
|
+
|
|
29
|
+
# -- Extraction helpers ------------------------------------------------
|
|
30
|
+
|
|
31
|
+
def _extract_scores(self) -> dict[str, dict[str, float | None]]:
|
|
32
|
+
"""Extract metric scores per strategy per task.
|
|
33
|
+
|
|
34
|
+
Returns ``{strategy: {task_metric: score}}``.
|
|
35
|
+
"""
|
|
36
|
+
scores: dict[str, dict[str, float | None]] = {}
|
|
37
|
+
for strategy_name, raw in self.results.items():
|
|
38
|
+
strategy_scores: dict[str, float | None] = {}
|
|
39
|
+
task_results = raw.get("results", {})
|
|
40
|
+
for task_name, metrics in task_results.items():
|
|
41
|
+
for metric_key, value in metrics.items():
|
|
42
|
+
if metric_key.endswith(",none"):
|
|
43
|
+
metric_key = metric_key[: -len(",none")]
|
|
44
|
+
if isinstance(value, (int, float)):
|
|
45
|
+
key = f"{task_name}/{metric_key}"
|
|
46
|
+
strategy_scores[key] = round(float(value), 4)
|
|
47
|
+
scores[strategy_name] = strategy_scores
|
|
48
|
+
return scores
|
|
49
|
+
|
|
50
|
+
# -- Formatting --------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def print_table(self) -> str:
|
|
53
|
+
"""Print a formatted comparison table and return it as a string.
|
|
54
|
+
|
|
55
|
+
Rows = metric (task/metric_name), Columns = strategies.
|
|
56
|
+
"""
|
|
57
|
+
scores = self._extract_scores()
|
|
58
|
+
strategies = list(scores.keys())
|
|
59
|
+
|
|
60
|
+
# Collect all unique metric keys across strategies
|
|
61
|
+
all_metrics: list[str] = []
|
|
62
|
+
seen: set[str] = set()
|
|
63
|
+
for strat_scores in scores.values():
|
|
64
|
+
for k in strat_scores:
|
|
65
|
+
if k not in seen:
|
|
66
|
+
all_metrics.append(k)
|
|
67
|
+
seen.add(k)
|
|
68
|
+
|
|
69
|
+
if not all_metrics:
|
|
70
|
+
msg = "(No benchmark results to display)"
|
|
71
|
+
print(msg)
|
|
72
|
+
return msg
|
|
73
|
+
|
|
74
|
+
# Column widths
|
|
75
|
+
metric_col_width = max(len(m) for m in all_metrics)
|
|
76
|
+
metric_col_width = max(metric_col_width, len("Metric"))
|
|
77
|
+
strat_col_width = max(max((len(s) for s in strategies), default=8), 10)
|
|
78
|
+
|
|
79
|
+
# Header
|
|
80
|
+
header = f"{'Metric':<{metric_col_width}}"
|
|
81
|
+
for s in strategies:
|
|
82
|
+
header += f" {s:>{strat_col_width}}"
|
|
83
|
+
separator = "-" * len(header)
|
|
84
|
+
|
|
85
|
+
lines = [
|
|
86
|
+
f"Model: {self.model}",
|
|
87
|
+
f"Tasks: {', '.join(self.tasks)}",
|
|
88
|
+
"",
|
|
89
|
+
header,
|
|
90
|
+
separator,
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Find best score per metric for highlighting
|
|
94
|
+
for metric in all_metrics:
|
|
95
|
+
vals = {s: scores[s].get(metric) for s in strategies}
|
|
96
|
+
best_val = max(
|
|
97
|
+
(v for v in vals.values() if v is not None), default=None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
row = f"{metric:<{metric_col_width}}"
|
|
101
|
+
for s in strategies:
|
|
102
|
+
v = vals[s]
|
|
103
|
+
if v is None:
|
|
104
|
+
cell = "—"
|
|
105
|
+
elif v == best_val and len(strategies) > 1:
|
|
106
|
+
cell = f"{v:.4f} ★"
|
|
107
|
+
else:
|
|
108
|
+
cell = f"{v:.4f}"
|
|
109
|
+
row += f" {cell:>{strat_col_width}}"
|
|
110
|
+
lines.append(row)
|
|
111
|
+
|
|
112
|
+
lines.append(separator)
|
|
113
|
+
table = "\n".join(lines)
|
|
114
|
+
print(table)
|
|
115
|
+
return table
|
|
116
|
+
|
|
117
|
+
def best_strategy(self, metric: str) -> str | None:
|
|
118
|
+
"""Return the strategy name with the highest score for *metric*.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
metric : str
|
|
123
|
+
Metric key like ``"gsm8k/acc"`` or ``"mmlu/acc"``.
|
|
124
|
+
"""
|
|
125
|
+
scores = self._extract_scores()
|
|
126
|
+
best_name: str | None = None
|
|
127
|
+
best_val: float | None = None
|
|
128
|
+
for strat, strat_scores in scores.items():
|
|
129
|
+
val = strat_scores.get(metric)
|
|
130
|
+
if val is not None and (best_val is None or val > best_val):
|
|
131
|
+
best_val = val
|
|
132
|
+
best_name = strat
|
|
133
|
+
return best_name
|
|
134
|
+
|
|
135
|
+
# -- Export ------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> dict[str, Any]:
|
|
138
|
+
"""Return a JSON-serialisable dictionary."""
|
|
139
|
+
return {
|
|
140
|
+
"model": self.model,
|
|
141
|
+
"tasks": self.tasks,
|
|
142
|
+
"scores": self._extract_scores(),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def to_json(self, path: str) -> None:
|
|
146
|
+
"""Write comparison results to a JSON file."""
|
|
147
|
+
with open(path, "w") as f:
|
|
148
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
149
|
+
|
|
150
|
+
def to_csv(self, path: str) -> None:
|
|
151
|
+
"""Write comparison results to a CSV file."""
|
|
152
|
+
import csv
|
|
153
|
+
|
|
154
|
+
scores = self._extract_scores()
|
|
155
|
+
strategies = list(scores.keys())
|
|
156
|
+
|
|
157
|
+
all_metrics: list[str] = []
|
|
158
|
+
seen: set[str] = set()
|
|
159
|
+
for strat_scores in scores.values():
|
|
160
|
+
for k in strat_scores:
|
|
161
|
+
if k not in seen:
|
|
162
|
+
all_metrics.append(k)
|
|
163
|
+
seen.add(k)
|
|
164
|
+
|
|
165
|
+
with open(path, "w", newline="") as f:
|
|
166
|
+
writer = csv.writer(f)
|
|
167
|
+
writer.writerow(["metric"] + strategies)
|
|
168
|
+
for metric in all_metrics:
|
|
169
|
+
row = [metric]
|
|
170
|
+
for s in strategies:
|
|
171
|
+
val = scores[s].get(metric)
|
|
172
|
+
row.append(f"{val:.4f}" if val is not None else "")
|
|
173
|
+
writer.writerow(row)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI, TUI, and terminal interaction helpers for ellements."""
|
|
2
|
+
|
|
3
|
+
from .adapters import (
|
|
4
|
+
guideline_provider_from_library,
|
|
5
|
+
persona_provider_from_library,
|
|
6
|
+
)
|
|
7
|
+
from .agent_tui import (
|
|
8
|
+
BUILTIN_COMMANDS,
|
|
9
|
+
AgentRunner,
|
|
10
|
+
AgentTUI,
|
|
11
|
+
FileSaveHandler,
|
|
12
|
+
GuidelineProvider,
|
|
13
|
+
Mode,
|
|
14
|
+
PersonaProvider,
|
|
15
|
+
RunResult,
|
|
16
|
+
SaveHandler,
|
|
17
|
+
SlashCommand,
|
|
18
|
+
TuiConfig,
|
|
19
|
+
TuiControls,
|
|
20
|
+
run_agent_tui,
|
|
21
|
+
)
|
|
22
|
+
from .components import (
|
|
23
|
+
ISSUE_STYLES,
|
|
24
|
+
ROLE_STYLES,
|
|
25
|
+
app_header,
|
|
26
|
+
chat_input_prompt,
|
|
27
|
+
chat_message,
|
|
28
|
+
error,
|
|
29
|
+
issue_line,
|
|
30
|
+
issues_block,
|
|
31
|
+
key_value_table,
|
|
32
|
+
result_json,
|
|
33
|
+
result_markdown,
|
|
34
|
+
section_rule,
|
|
35
|
+
stats_footer,
|
|
36
|
+
step_log,
|
|
37
|
+
success,
|
|
38
|
+
tool_use_badge,
|
|
39
|
+
warning,
|
|
40
|
+
welcome_banner,
|
|
41
|
+
)
|
|
42
|
+
from .printer import CliPrinter, EventRenderer
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"AgentRunner",
|
|
46
|
+
"AgentTUI",
|
|
47
|
+
"BUILTIN_COMMANDS",
|
|
48
|
+
"CliPrinter",
|
|
49
|
+
"EventRenderer",
|
|
50
|
+
"FileSaveHandler",
|
|
51
|
+
"GuidelineProvider",
|
|
52
|
+
"ISSUE_STYLES",
|
|
53
|
+
"Mode",
|
|
54
|
+
"PersonaProvider",
|
|
55
|
+
"ROLE_STYLES",
|
|
56
|
+
"RunResult",
|
|
57
|
+
"SaveHandler",
|
|
58
|
+
"SlashCommand",
|
|
59
|
+
"TuiConfig",
|
|
60
|
+
"TuiControls",
|
|
61
|
+
"app_header",
|
|
62
|
+
"chat_input_prompt",
|
|
63
|
+
"chat_message",
|
|
64
|
+
"error",
|
|
65
|
+
"guideline_provider_from_library",
|
|
66
|
+
"issue_line",
|
|
67
|
+
"issues_block",
|
|
68
|
+
"key_value_table",
|
|
69
|
+
"persona_provider_from_library",
|
|
70
|
+
"result_json",
|
|
71
|
+
"result_markdown",
|
|
72
|
+
"run_agent_tui",
|
|
73
|
+
"section_rule",
|
|
74
|
+
"stats_footer",
|
|
75
|
+
"step_log",
|
|
76
|
+
"success",
|
|
77
|
+
"tool_use_badge",
|
|
78
|
+
"warning",
|
|
79
|
+
"welcome_banner",
|
|
80
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Adapters between ellements core types and TUI provider Protocols.
|
|
2
|
+
|
|
3
|
+
The :mod:`ellements.cli.agent_tui` module defines ``PersonaProvider`` /
|
|
4
|
+
``GuidelineProvider`` Protocols so the TUI never imports an
|
|
5
|
+
``AgentController`` directly. This module supplies the canonical
|
|
6
|
+
adapters that bridge a :class:`~ellements.core.PersonaLibrary` /
|
|
7
|
+
:class:`~ellements.core.GuidelineLibrary` + controller pair to those
|
|
8
|
+
Protocols.
|
|
9
|
+
|
|
10
|
+
Apps that don't use ``AgentController`` can write their own adapters
|
|
11
|
+
and pass them to :class:`~ellements.cli.agent_tui.TuiConfig` directly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .agent_tui import GuidelineProvider, PersonaProvider
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _LibraryPersonaProvider:
|
|
22
|
+
"""Bridge a ``PersonaLibrary`` + controller to ``PersonaProvider``."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, library: Any, controller: Any) -> None:
|
|
25
|
+
self._library = library
|
|
26
|
+
self._controller = controller
|
|
27
|
+
|
|
28
|
+
def list_ids(self) -> list[str]:
|
|
29
|
+
return list(self._library.list_ids())
|
|
30
|
+
|
|
31
|
+
def current_id(self) -> str | None:
|
|
32
|
+
return getattr(self._controller, "current_persona_id", None)
|
|
33
|
+
|
|
34
|
+
def select(self, persona_id: str) -> None:
|
|
35
|
+
self._controller.set_persona_by_id(persona_id)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _LibraryGuidelineProvider:
|
|
39
|
+
"""Bridge a ``GuidelineLibrary`` + controller to ``GuidelineProvider``."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, library: Any, controller: Any) -> None:
|
|
42
|
+
self._library = library
|
|
43
|
+
self._controller = controller
|
|
44
|
+
|
|
45
|
+
def list_ids(self) -> list[str]:
|
|
46
|
+
return list(self._library.list_ids())
|
|
47
|
+
|
|
48
|
+
def current_id(self) -> str | None:
|
|
49
|
+
return getattr(self._controller, "current_guideline_id", None)
|
|
50
|
+
|
|
51
|
+
def select(self, guideline_id: str) -> None:
|
|
52
|
+
self._controller.set_guideline_by_id(guideline_id)
|
|
53
|
+
|
|
54
|
+
def clear(self) -> None:
|
|
55
|
+
self._controller.clear_guideline()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def persona_provider_from_library(library: Any, controller: Any) -> PersonaProvider:
|
|
59
|
+
"""Adapt a :class:`PersonaLibrary` + controller pair to the TUI."""
|
|
60
|
+
return _LibraryPersonaProvider(library, controller)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def guideline_provider_from_library(
|
|
64
|
+
library: Any, controller: Any
|
|
65
|
+
) -> GuidelineProvider:
|
|
66
|
+
"""Adapt a :class:`GuidelineLibrary` + controller pair to the TUI."""
|
|
67
|
+
return _LibraryGuidelineProvider(library, controller)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"guideline_provider_from_library",
|
|
72
|
+
"persona_provider_from_library",
|
|
73
|
+
]
|