fluxloop-cli 0.2.30__tar.gz → 0.2.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/PKG-INFO +5 -3
  2. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/README.md +2 -2
  3. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/__init__.py +1 -1
  4. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/arg_binder.py +2 -3
  5. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/config.py +0 -1
  6. fluxloop_cli-0.2.32/fluxloop_cli/commands/evaluate.py +194 -0
  7. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/init.py +1 -1
  8. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/record.py +0 -1
  9. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/run.py +0 -1
  10. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/status.py +1 -2
  11. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/config_loader.py +0 -2
  12. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/artifacts.py +1 -1
  13. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/config.py +41 -2
  14. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/__init__.py +0 -0
  15. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/aggregator.py +626 -0
  16. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/generator.py +759 -0
  17. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/pdf_exporter.py +71 -0
  18. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/pipeline.py +105 -0
  19. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/renderer.py +394 -0
  20. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/rules.py +1 -1
  21. fluxloop_cli-0.2.32/fluxloop_cli/evaluation/templates/report.html.j2 +7072 -0
  22. fluxloop_cli-0.2.32/fluxloop_cli/input_generator.py +261 -0
  23. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/llm_generator.py +23 -23
  24. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/main.py +0 -2
  25. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/project_paths.py +0 -1
  26. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/runner.py +2 -1
  27. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/templates.py +201 -252
  28. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/testing/pytest_plugin.py +13 -2
  29. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/PKG-INFO +5 -3
  30. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/SOURCES.txt +7 -0
  31. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/requires.txt +2 -0
  32. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/pyproject.toml +9 -1
  33. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_arg_binder.py +1 -1
  34. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_conversation_supervisor.py +0 -2
  35. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_evaluate_command.py +55 -68
  36. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_evaluation_llm.py +5 -4
  37. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_input_generator.py +0 -2
  38. fluxloop_cli-0.2.30/fluxloop_cli/commands/evaluate.py +0 -219
  39. fluxloop_cli-0.2.30/fluxloop_cli/input_generator.py +0 -138
  40. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/__init__.py +0 -0
  41. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/doctor.py +0 -0
  42. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/generate.py +0 -0
  43. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/parse.py +0 -0
  44. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/config_schema.py +0 -0
  45. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/constants.py +0 -0
  46. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/conversation_supervisor.py +0 -0
  47. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/environment.py +0 -0
  48. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/__init__.py +0 -0
  49. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
  50. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
  51. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/core.py +0 -0
  52. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
  53. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/reporting/html.py +0 -0
  54. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
  55. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/success.py +0 -0
  56. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/llm.py +0 -0
  57. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
  58. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/base.py +0 -0
  59. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/information_completeness.py +0 -0
  60. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/intent_recognition.py +0 -0
  61. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/response_clarity.py +0 -0
  62. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/response_consistency.py +0 -0
  63. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/target_loader.py +0 -0
  64. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/testing/__init__.py +0 -0
  65. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/testing/types.py +0 -0
  66. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/validators.py +0 -0
  67. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  68. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  69. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/top_level.txt +0 -0
  70. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/setup.cfg +0 -0
  71. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_analysis_recommendations.py +0 -0
  72. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_config_command.py +0 -0
  73. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_init_pytest_template.py +0 -0
  74. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_prompt_library.py +0 -0
  75. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_pytest_plugin.py +0 -0
  76. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_run_command.py +0 -0
  77. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_runner_multi_turn.py +0 -0
  78. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_success_criteria.py +0 -0
  79. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_target_loader.py +0 -0
  80. {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_testing_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.30
3
+ Version: 0.2.32
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -27,6 +27,8 @@ Requires-Dist: rich>=13.0
27
27
  Requires-Dist: python-dotenv>=1.0.0
28
28
  Requires-Dist: fluxloop>=0.1.0
29
29
  Requires-Dist: ruamel.yaml>=0.17.0
30
+ Requires-Dist: Jinja2>=3.0
31
+ Requires-Dist: weasyprint>=61.0
30
32
  Provides-Extra: dev
31
33
  Requires-Dist: pytest>=7.0; extra == "dev"
32
34
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -67,7 +69,7 @@ The legacy `setting.yaml` is still supported, but new projects created with
67
69
  - `fluxloop generate inputs` – produce input variations for the active project
68
70
  - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
69
71
  - `fluxloop parse experiment` – convert experiment outputs into readable artifacts and emit structured per-trace JSON at `per_trace_analysis/per_trace.jsonl`
70
- - `fluxloop evaluate experiment` – score experiment outputs using rule-based and LLM evaluators (requires `fluxloop parse` output or `--per-trace`), generate reports with success criteria, analysis, and customizable templates
72
+ - `fluxloop evaluate experiment` – run the LLM-driven evaluation pipeline (LLM-PT rule aggregation → LLM-OV HTML render). Requires the parsed per-trace file (or `--per-trace`) and writes an interactive report to `evaluation_report/report.html` by default.
71
73
  - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
72
74
  - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
73
75
  - `fluxloop doctor` – summarize Python, FluxLoop CLI/MCP, and MCP index state for the active environment
@@ -105,7 +107,7 @@ Evaluation now follows a two-step process so that multi-turn context is preserve
105
107
 
106
108
  1. `fluxloop run experiment` – produce `trace_summary.jsonl` (and optionally `observations.jsonl`).
107
109
  2. `fluxloop parse experiment <experiment_dir>` – generate markdown summaries and a structured artifact at `per_trace_analysis/per_trace.jsonl`.
108
- 3. `fluxloop evaluate experiment <experiment_dir>` – consume the structured file to calculate scores and reports.
110
+ 3. `fluxloop evaluate experiment <experiment_dir>` – consume that structured file, run LLM-based per-trace + overall analysis, and emit an interactive dashboard at `<experiment_dir>/evaluation_report/report.html` (override with `--output`).
109
111
 
110
112
  `fluxloop evaluate` exits early with guidance when the per-trace artifact is missing. If you relocate the file, supply an explicit path with `--per-trace /path/to/per_trace.jsonl`.
111
113
 
@@ -26,7 +26,7 @@ The legacy `setting.yaml` is still supported, but new projects created with
26
26
  - `fluxloop generate inputs` – produce input variations for the active project
27
27
  - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
28
28
  - `fluxloop parse experiment` – convert experiment outputs into readable artifacts and emit structured per-trace JSON at `per_trace_analysis/per_trace.jsonl`
29
- - `fluxloop evaluate experiment` – score experiment outputs using rule-based and LLM evaluators (requires `fluxloop parse` output or `--per-trace`), generate reports with success criteria, analysis, and customizable templates
29
+ - `fluxloop evaluate experiment` – run the LLM-driven evaluation pipeline (LLM-PT rule aggregation → LLM-OV HTML render). Requires the parsed per-trace file (or `--per-trace`) and writes an interactive report to `evaluation_report/report.html` by default.
30
30
  - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
31
31
  - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
32
32
  - `fluxloop doctor` – summarize Python, FluxLoop CLI/MCP, and MCP index state for the active environment
@@ -64,7 +64,7 @@ Evaluation now follows a two-step process so that multi-turn context is preserve
64
64
 
65
65
  1. `fluxloop run experiment` – produce `trace_summary.jsonl` (and optionally `observations.jsonl`).
66
66
  2. `fluxloop parse experiment <experiment_dir>` – generate markdown summaries and a structured artifact at `per_trace_analysis/per_trace.jsonl`.
67
- 3. `fluxloop evaluate experiment <experiment_dir>` – consume the structured file to calculate scores and reports.
67
+ 3. `fluxloop evaluate experiment <experiment_dir>` – consume that structured file, run LLM-based per-trace + overall analysis, and emit an interactive dashboard at `<experiment_dir>/evaluation_report/report.html` (override with `--output`).
68
68
 
69
69
  `fluxloop evaluate` exits early with guidance when the per-trace artifact is missing. If you relocate the file, supply an explicit path with `--per-trace /path/to/per_trace.jsonl`.
70
70
 
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.30"
5
+ __version__ = "0.2.32"
6
6
 
7
7
  from .main import app
8
8
 
@@ -7,6 +7,8 @@ import json
7
7
  from pathlib import Path
8
8
  from typing import Any, Callable, Dict, Optional, Sequence
9
9
 
10
+ from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig, PersonaConfig
11
+
10
12
 
11
13
  class _AttrDict(dict):
12
14
  """Dictionary that also supports attribute access for keys."""
@@ -26,8 +28,6 @@ class _AttrDict(dict):
26
28
  except KeyError as exc: # pragma: no cover
27
29
  raise AttributeError(item) from exc
28
30
 
29
- from fluxloop.schemas import ExperimentConfig, ReplayArgsConfig, PersonaConfig
30
-
31
31
 
32
32
  class _AwaitableNone:
33
33
  """Simple awaitable that resolves to ``None``."""
@@ -278,7 +278,6 @@ class ArgBinder:
278
278
 
279
279
  def _record(args: Any, kwargs: Any) -> None:
280
280
  messages.append((args, kwargs))
281
- pretty = args[0] if len(args) == 1 and not kwargs else {"args": args, "kwargs": kwargs}
282
281
 
283
282
  def send(*args: Any, **kwargs: Any) -> _AwaitableNone:
284
283
  _record(args, kwargs)
@@ -14,7 +14,6 @@ from rich.syntax import Syntax
14
14
  from rich.table import Table
15
15
 
16
16
  from ..config_loader import load_experiment_config
17
- from ..templates import create_env_file, create_gitignore, create_sample_agent
18
17
  from ..constants import DEFAULT_CONFIG_PATH, DEFAULT_ROOT_DIR_NAME
19
18
  from ..config_schema import CONFIG_SECTION_FILENAMES
20
19
  from ..project_paths import (
@@ -0,0 +1,194 @@
1
+ """Evaluate command for generating interactive reports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import shutil
9
+ from dataclasses import asdict
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import typer
14
+ import yaml
15
+ from rich.console import Console
16
+
17
+ from ..environment import load_env_chain
18
+ from ..evaluation import load_evaluation_config
19
+ from ..evaluation.artifacts import load_per_trace_records
20
+ from ..evaluation.report.pipeline import ReportPipeline
21
+
22
+ console = Console()
23
+ app = typer.Typer(help="Evaluate experiment outputs and generate interactive reports.")
24
+
25
+
26
+ def _load_yaml_file(path: Optional[Path]) -> dict:
27
+ if not path or not path.exists():
28
+ return {}
29
+ with path.open("r", encoding="utf-8") as handle:
30
+ data = yaml.safe_load(handle) or {}
31
+ if isinstance(data, dict):
32
+ return data
33
+ return {}
34
+
35
+
36
+ def _resolve_project_root(config_path: Path) -> Path:
37
+ config_dir = config_path.parent
38
+ return config_dir.parent if config_dir.name == "configs" else config_dir
39
+
40
+
41
+ def _find_config_file(config_path: Path, filename: str) -> Optional[Path]:
42
+ config_dir = config_path.parent
43
+ project_root = _resolve_project_root(config_path)
44
+ candidates = [
45
+ config_dir / filename,
46
+ project_root / "configs" / filename,
47
+ project_root / filename,
48
+ ]
49
+ for candidate in candidates:
50
+ if candidate.exists():
51
+ return candidate
52
+ return None
53
+
54
+
55
+ def _prepare_output_directory(path: Path, overwrite: bool) -> None:
56
+ if path.exists():
57
+ if not overwrite:
58
+ raise typer.BadParameter(
59
+ f"Output directory already exists: {path}. Use --overwrite to replace it."
60
+ )
61
+ shutil.rmtree(path)
62
+ path.mkdir(parents=True, exist_ok=True)
63
+
64
+
65
+ @app.command()
66
+ def experiment(
67
+ experiment_dir: Path = typer.Argument(
68
+ ...,
69
+ help="Path to the experiment output directory",
70
+ exists=True,
71
+ dir_okay=True,
72
+ file_okay=False,
73
+ resolve_path=True,
74
+ ),
75
+ config: Path = typer.Option(
76
+ Path("configs/evaluation.yaml"),
77
+ "--config",
78
+ "-c",
79
+ help="Path to evaluation configuration file",
80
+ ),
81
+ output: Path = typer.Option(
82
+ Path("evaluation_report"),
83
+ "--output",
84
+ "-o",
85
+ help="Output directory name (relative to the experiment directory)",
86
+ ),
87
+ overwrite: bool = typer.Option(
88
+ False,
89
+ "--overwrite",
90
+ help="Overwrite output directory if it already exists",
91
+ ),
92
+ llm_api_key: Optional[str] = typer.Option(
93
+ None,
94
+ "--llm-api-key",
95
+ help="LLM API key for report generation (optional)",
96
+ envvar="FLUXLOOP_LLM_API_KEY",
97
+ ),
98
+ per_trace: Optional[Path] = typer.Option(
99
+ None,
100
+ "--per-trace",
101
+ help="Path to structured per-trace JSONL generated by `fluxloop parse`",
102
+ ),
103
+ verbose: bool = typer.Option(
104
+ False,
105
+ "--verbose",
106
+ help="Enable verbose logging",
107
+ ),
108
+ ):
109
+ """
110
+ Evaluate experiment outputs and generate an interactive HTML report.
111
+ """
112
+
113
+ logging.basicConfig(
114
+ level=logging.DEBUG if verbose else logging.INFO,
115
+ format="%(message)s",
116
+ )
117
+
118
+ resolved_experiment_dir = experiment_dir.resolve()
119
+ if not resolved_experiment_dir.is_dir():
120
+ raise typer.BadParameter(f"Experiment directory not found: {resolved_experiment_dir}")
121
+
122
+ config_path = config.resolve() if config.is_absolute() else (Path.cwd() / config).resolve()
123
+
124
+ if per_trace is not None:
125
+ per_trace_path = per_trace.resolve() if per_trace.is_absolute() else (Path.cwd() / per_trace).resolve()
126
+ else:
127
+ per_trace_path = resolved_experiment_dir / "per_trace_analysis" / "per_trace.jsonl"
128
+
129
+ per_trace_records = load_per_trace_records(resolved_experiment_dir, per_trace_path)
130
+ trace_summaries = [record.trace for record in per_trace_records]
131
+ if not trace_summaries:
132
+ raise typer.BadParameter("No traces found in per-trace artifacts.")
133
+
134
+ try:
135
+ evaluation_config = load_evaluation_config(config_path)
136
+ except FileNotFoundError as exc:
137
+ raise typer.BadParameter(str(exc)) from exc
138
+ except Exception as exc: # noqa: BLE001
139
+ raise typer.BadParameter(f"Failed to load evaluation config: {exc}") from exc
140
+
141
+ def _log_env_error(path: Path, exc: Exception) -> None:
142
+ console.log(
143
+ f"[yellow]Warning:[/yellow] Failed to load environment from {path}: {exc}"
144
+ )
145
+
146
+ load_env_chain(
147
+ evaluation_config.get_source_dir(),
148
+ refresh_config=True,
149
+ on_error=_log_env_error,
150
+ )
151
+
152
+ if llm_api_key is None:
153
+ llm_api_key = os.getenv("FLUXLOOP_LLM_API_KEY") or os.getenv("OPENAI_API_KEY")
154
+
155
+ output_dir = output if output.is_absolute() else (resolved_experiment_dir / output)
156
+ _prepare_output_directory(output_dir, overwrite)
157
+
158
+ input_config_path = _find_config_file(config_path, "input.yaml")
159
+ project_config_path = _find_config_file(config_path, "project.yaml")
160
+
161
+ input_config = _load_yaml_file(input_config_path)
162
+ project_config = _load_yaml_file(project_config_path)
163
+
164
+ config_bundle = {
165
+ "name": project_config.get("name") or resolved_experiment_dir.name,
166
+ "evaluation": asdict(evaluation_config),
167
+ "input": input_config,
168
+ }
169
+
170
+ pipeline = ReportPipeline(
171
+ config=config_bundle,
172
+ output_dir=output_dir,
173
+ api_key=llm_api_key,
174
+ )
175
+
176
+ message_lines = [
177
+ f"📊 Evaluating experiment at [cyan]{resolved_experiment_dir}[/cyan]",
178
+ f"⚙️ Config: [magenta]{config_path}[/magenta]",
179
+ f"🧵 Per-trace data: [blue]{per_trace_path}[/blue]",
180
+ f"📁 Output: [green]{output_dir}[/green]",
181
+ ]
182
+ console.print("\n".join(message_lines))
183
+
184
+ artifacts = asyncio.run(pipeline.run(trace_summaries))
185
+ console.print(f"\n✅ Report ready: [bold cyan]{artifacts.html_path}[/bold cyan]")
186
+ if artifacts.pdf_path:
187
+ console.print(f"🖨️ PDF ready: [bold green]{artifacts.pdf_path}[/bold green]")
188
+ else:
189
+ console.print(
190
+ "⚠️ PDF export skipped (WeasyPrint is unavailable or PDF rendering failed)."
191
+ )
192
+
193
+
194
+
@@ -188,7 +188,7 @@ def project(
188
188
  console.print("4. Generate inputs: [green]fluxloop generate inputs[/green]")
189
189
  console.print("5. Run the experiment: [green]fluxloop run experiment[/green]")
190
190
  console.print("6. Parse outputs: [green]fluxloop parse experiment[/green]")
191
- console.print("7. Evaluate results (optional): [green]fluxloop evaluate experiment[/green]")
191
+ console.print("7. Generate the interactive report (optional): [green]fluxloop evaluate experiment[/green]")
192
192
  console.print("8. Diagnose environment anytime: [green]fluxloop doctor[/green]")
193
193
 
194
194
 
@@ -16,7 +16,6 @@ from ..project_paths import (
16
16
  resolve_project_dir,
17
17
  resolve_config_section_path,
18
18
  )
19
- from ..config_schema import CONFIG_SECTION_FILENAMES
20
19
 
21
20
 
22
21
  app = typer.Typer()
@@ -9,7 +9,6 @@ from typing import Callable, Optional
9
9
 
10
10
  import typer
11
11
  from rich.console import Console
12
- from rich.live import Live
13
12
  from rich.panel import Panel
14
13
  from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeRemainingColumn
15
14
  from rich.table import Table
@@ -15,7 +15,6 @@ from ..config_schema import CONFIG_SECTION_FILENAMES, CONFIG_REQUIRED_KEYS
15
15
  from ..project_paths import (
16
16
  resolve_config_path,
17
17
  resolve_config_directory,
18
- resolve_root_dir,
19
18
  resolve_project_relative,
20
19
  )
21
20
 
@@ -79,7 +78,7 @@ def check(
79
78
  from fluxloop.client import FluxLoopClient
80
79
 
81
80
  config = get_config()
82
- client = FluxLoopClient()
81
+ _client = FluxLoopClient() # noqa: F841 - instantiated for potential future use
83
82
 
84
83
  # Try to connect (this would need a health endpoint)
85
84
  status_table.add_row(
@@ -12,7 +12,6 @@ from pydantic import ValidationError
12
12
  from .project_paths import resolve_config_path
13
13
  from .config_schema import (
14
14
  CONFIG_SECTION_FILENAMES,
15
- CONFIG_SECTION_ORDER,
16
15
  CONFIG_REQUIRED_KEYS,
17
16
  iter_section_paths,
18
17
  is_legacy_config,
@@ -52,7 +51,6 @@ def load_experiment_config(
52
51
 
53
52
  for section_path in iter_section_paths(project_root):
54
53
  if not section_path.exists():
55
- key = section_path.name
56
54
  logical_key = _section_key_from_filename(section_path.name)
57
55
  if logical_key in CONFIG_REQUIRED_KEYS:
58
56
  missing_required.append(section_path.name)
@@ -7,7 +7,7 @@ from __future__ import annotations
7
7
  import json
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
- from typing import Any, Dict, Iterable, List, Optional
10
+ from typing import Any, Dict, List, Optional
11
11
 
12
12
 
13
13
  @dataclass
@@ -229,6 +229,8 @@ class EvaluationConfig:
229
229
  additional_analysis: AdditionalAnalysisConfig = field(default_factory=AdditionalAnalysisConfig)
230
230
  report: ReportConfig = field(default_factory=ReportConfig)
231
231
  advanced: AdvancedConfig = field(default_factory=AdvancedConfig)
232
+ metrics: Dict[str, Any] = field(default_factory=dict)
233
+ efficiency: Dict[str, Any] = field(default_factory=dict)
232
234
 
233
235
  def set_source_dir(self, source_dir: Path) -> None:
234
236
  """Remember the directory where this config file was loaded from."""
@@ -383,12 +385,45 @@ def _parse_limits(raw: Any) -> LimitsConfig:
383
385
  )
384
386
 
385
387
 
388
+ def _parse_metrics(raw: Any) -> Dict[str, Any]:
389
+ if raw is None:
390
+ return {}
391
+ if not isinstance(raw, dict):
392
+ raise ValueError("metrics must be a mapping")
393
+ normalized: Dict[str, Any] = {}
394
+ for key, value in raw.items():
395
+ if not isinstance(value, dict):
396
+ raise ValueError(f"metrics.{key} must be a mapping")
397
+ normalized[key] = dict(value)
398
+ return normalized
399
+
400
+
401
+ def _parse_efficiency(raw: Any) -> Dict[str, Any]:
402
+ if raw is None:
403
+ return {}
404
+ if not isinstance(raw, dict):
405
+ raise ValueError("efficiency must be a mapping")
406
+ normalized: Dict[str, Any] = {}
407
+ for key, value in raw.items():
408
+ if not isinstance(value, dict):
409
+ raise ValueError(f"efficiency.{key} must be a mapping")
410
+ normalized[key] = dict(value)
411
+ return normalized
412
+
413
+
386
414
  def _parse_evaluation_goal(raw: Any) -> EvaluationGoalConfig:
387
415
  if raw is None:
388
416
  return EvaluationGoalConfig()
417
+
418
+ if isinstance(raw, str):
419
+ # Backwards compatibility for older templates that used a bare string.
420
+ return EvaluationGoalConfig(text=raw.strip())
421
+
389
422
  if not isinstance(raw, dict):
390
- raise ValueError("evaluation_goal must be a mapping")
391
- text = str(raw.get("text", "")) if raw.get("text") is not None else ""
423
+ raise ValueError("evaluation_goal must be a mapping or string")
424
+
425
+ text_value = raw.get("text")
426
+ text = str(text_value) if text_value is not None else ""
392
427
  return EvaluationGoalConfig(text=text)
393
428
 
394
429
 
@@ -711,6 +746,8 @@ def load_evaluation_config(path: Path) -> EvaluationConfig:
711
746
  additional_analysis = _parse_additional_analysis(data.get("additional_analysis"))
712
747
  report = _parse_report(data.get("report"))
713
748
  advanced = _parse_advanced(data.get("advanced"))
749
+ metrics = _parse_metrics(data.get("metrics"))
750
+ efficiency = _parse_efficiency(data.get("efficiency"))
714
751
 
715
752
  config_dir = path.parent.resolve()
716
753
 
@@ -723,6 +760,8 @@ def load_evaluation_config(path: Path) -> EvaluationConfig:
723
760
  additional_analysis=additional_analysis,
724
761
  report=report,
725
762
  advanced=advanced,
763
+ metrics=metrics,
764
+ efficiency=efficiency,
726
765
  )
727
766
 
728
767
  config.set_source_dir(config_dir)