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.
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/PKG-INFO +5 -3
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/README.md +2 -2
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/__init__.py +1 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/arg_binder.py +2 -3
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/config.py +0 -1
- fluxloop_cli-0.2.32/fluxloop_cli/commands/evaluate.py +194 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/init.py +1 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/record.py +0 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/run.py +0 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/status.py +1 -2
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/config_loader.py +0 -2
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/artifacts.py +1 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/config.py +41 -2
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/__init__.py +0 -0
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/aggregator.py +626 -0
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/generator.py +759 -0
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/pdf_exporter.py +71 -0
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/pipeline.py +105 -0
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/report/renderer.py +394 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/rules.py +1 -1
- fluxloop_cli-0.2.32/fluxloop_cli/evaluation/templates/report.html.j2 +7072 -0
- fluxloop_cli-0.2.32/fluxloop_cli/input_generator.py +261 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/llm_generator.py +23 -23
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/main.py +0 -2
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/project_paths.py +0 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/runner.py +2 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/templates.py +201 -252
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/testing/pytest_plugin.py +13 -2
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/PKG-INFO +5 -3
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/SOURCES.txt +7 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/requires.txt +2 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/pyproject.toml +9 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_arg_binder.py +1 -1
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_conversation_supervisor.py +0 -2
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_evaluate_command.py +55 -68
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_evaluation_llm.py +5 -4
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_input_generator.py +0 -2
- fluxloop_cli-0.2.30/fluxloop_cli/commands/evaluate.py +0 -219
- fluxloop_cli-0.2.30/fluxloop_cli/input_generator.py +0 -138
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/__init__.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/doctor.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/generate.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/commands/parse.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/config_schema.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/constants.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/conversation_supervisor.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/environment.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/__init__.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/core.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/reporting/html.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/engine/success.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/llm.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/base.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/information_completeness.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/intent_recognition.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/response_clarity.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/evaluation/prompts/response_consistency.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/target_loader.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/testing/__init__.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/testing/types.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli/validators.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/entry_points.txt +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/fluxloop_cli.egg-info/top_level.txt +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/setup.cfg +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_analysis_recommendations.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_config_command.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_init_pytest_template.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_prompt_library.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_pytest_plugin.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_run_command.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_runner_multi_turn.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_success_criteria.py +0 -0
- {fluxloop_cli-0.2.30 → fluxloop_cli-0.2.32}/tests/test_target_loader.py +0 -0
- {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.
|
|
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` –
|
|
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
|
|
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` –
|
|
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
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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)
|
|
File without changes
|