fluxloop-cli 0.2.29__tar.gz → 0.2.31__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 (78) hide show
  1. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/PKG-INFO +15 -3
  2. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/README.md +13 -2
  3. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/__init__.py +1 -1
  4. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/arg_binder.py +2 -3
  5. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/config.py +0 -1
  6. fluxloop_cli-0.2.31/fluxloop_cli/commands/evaluate.py +188 -0
  7. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/init.py +69 -1
  8. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/record.py +0 -1
  9. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/run.py +11 -2
  10. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/status.py +1 -2
  11. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/config_loader.py +0 -2
  12. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/artifacts.py +1 -1
  13. fluxloop_cli-0.2.31/fluxloop_cli/evaluation/report/__init__.py +0 -0
  14. fluxloop_cli-0.2.31/fluxloop_cli/evaluation/report/aggregator.py +568 -0
  15. fluxloop_cli-0.2.31/fluxloop_cli/evaluation/report/generator.py +716 -0
  16. fluxloop_cli-0.2.31/fluxloop_cli/evaluation/report/pipeline.py +83 -0
  17. fluxloop_cli-0.2.31/fluxloop_cli/evaluation/report/renderer.py +371 -0
  18. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/rules.py +1 -1
  19. fluxloop_cli-0.2.31/fluxloop_cli/input_generator.py +264 -0
  20. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/llm_generator.py +23 -23
  21. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/main.py +0 -2
  22. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/project_paths.py +0 -1
  23. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/runner.py +2 -1
  24. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/templates.py +226 -252
  25. fluxloop_cli-0.2.31/fluxloop_cli/testing/__init__.py +24 -0
  26. fluxloop_cli-0.2.31/fluxloop_cli/testing/pytest_plugin.py +432 -0
  27. fluxloop_cli-0.2.31/fluxloop_cli/testing/types.py +188 -0
  28. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli.egg-info/PKG-INFO +15 -3
  29. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli.egg-info/SOURCES.txt +12 -1
  30. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli.egg-info/requires.txt +1 -0
  31. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/pyproject.toml +2 -1
  32. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_arg_binder.py +1 -1
  33. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_conversation_supervisor.py +0 -2
  34. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_evaluate_command.py +49 -68
  35. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_evaluation_llm.py +6 -5
  36. fluxloop_cli-0.2.31/tests/test_init_pytest_template.py +62 -0
  37. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_input_generator.py +0 -2
  38. fluxloop_cli-0.2.31/tests/test_pytest_plugin.py +103 -0
  39. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_run_command.py +2 -4
  40. fluxloop_cli-0.2.31/tests/test_testing_types.py +66 -0
  41. fluxloop_cli-0.2.29/fluxloop_cli/commands/evaluate.py +0 -219
  42. fluxloop_cli-0.2.29/fluxloop_cli/input_generator.py +0 -138
  43. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/__init__.py +0 -0
  44. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/doctor.py +0 -0
  45. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/generate.py +0 -0
  46. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/commands/parse.py +0 -0
  47. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/config_schema.py +0 -0
  48. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/constants.py +0 -0
  49. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/conversation_supervisor.py +0 -0
  50. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/environment.py +0 -0
  51. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/__init__.py +0 -0
  52. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/config.py +0 -0
  53. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
  54. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
  55. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/core.py +0 -0
  56. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
  57. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/reporting/html.py +0 -0
  58. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
  59. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/engine/success.py +0 -0
  60. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/llm.py +0 -0
  61. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
  62. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/prompts/base.py +0 -0
  63. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/prompts/information_completeness.py +0 -0
  64. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/prompts/intent_recognition.py +0 -0
  65. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/prompts/response_clarity.py +0 -0
  66. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/evaluation/prompts/response_consistency.py +0 -0
  67. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/target_loader.py +0 -0
  68. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli/validators.py +0 -0
  69. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  70. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  71. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/fluxloop_cli.egg-info/top_level.txt +0 -0
  72. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/setup.cfg +0 -0
  73. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_analysis_recommendations.py +0 -0
  74. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_config_command.py +0 -0
  75. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_prompt_library.py +0 -0
  76. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_runner_multi_turn.py +0 -0
  77. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_success_criteria.py +0 -0
  78. {fluxloop_cli-0.2.29 → fluxloop_cli-0.2.31}/tests/test_target_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.29
3
+ Version: 0.2.31
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,7 @@ 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
30
31
  Provides-Extra: dev
31
32
  Requires-Dist: pytest>=7.0; extra == "dev"
32
33
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -67,10 +68,11 @@ The legacy `setting.yaml` is still supported, but new projects created with
67
68
  - `fluxloop generate inputs` – produce input variations for the active project
68
69
  - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
69
70
  - `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
71
+ - `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
72
  - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
72
73
  - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
73
74
  - `fluxloop doctor` – summarize Python, FluxLoop CLI/MCP, and MCP index state for the active environment
75
+ - `--yes/-y` (for `fluxloop run experiment`) – skip the interactive confirmation prompt, ideal for CI and the Pytest bridge
74
76
 
75
77
  ### Multi-turn supervisor options
76
78
 
@@ -88,13 +90,23 @@ These flags override the values in `configs/simulation.yaml` (`multi_turn` block
88
90
 
89
91
  Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
90
92
 
93
+ ## Pytest Bridge (0.2.29+)
94
+
95
+ - `fluxloop init pytest-template [project_root]` creates `tests/test_fluxloop_smoke.py`, already wired to the new `fluxloop_runner` fixture.
96
+ - Fixtures live in `fluxloop_cli.testing.pytest_plugin` and return a `FluxLoopTestResult`, so you can assert on `total_runs`, `success_rate`, or call `require_success()`.
97
+ - Full guide + CI example: see `docs/guides/pytest_bridge.md` (includes GitHub Actions workflow at `examples/ci/fluxloop_pytest.yml`).
98
+ - Typical workflow:
99
+ 1. `pip install -e packages/cli[dev]`
100
+ 2. `fluxloop init pytest-template .`
101
+ 3. `pytest -k fluxloop_smoke --maxfail=1`
102
+
91
103
  ## Evaluation Workflow
92
104
 
93
105
  Evaluation now follows a two-step process so that multi-turn context is preserved:
94
106
 
95
107
  1. `fluxloop run experiment` – produce `trace_summary.jsonl` (and optionally `observations.jsonl`).
96
108
  2. `fluxloop parse experiment <experiment_dir>` – generate markdown summaries and a structured artifact at `per_trace_analysis/per_trace.jsonl`.
97
- 3. `fluxloop evaluate experiment <experiment_dir>` – consume the structured file to calculate scores and reports.
109
+ 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`).
98
110
 
99
111
  `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`.
100
112
 
@@ -26,10 +26,11 @@ 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
33
+ - `--yes/-y` (for `fluxloop run experiment`) – skip the interactive confirmation prompt, ideal for CI and the Pytest bridge
33
34
 
34
35
  ### Multi-turn supervisor options
35
36
 
@@ -47,13 +48,23 @@ These flags override the values in `configs/simulation.yaml` (`multi_turn` block
47
48
 
48
49
  Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
49
50
 
51
+ ## Pytest Bridge (0.2.29+)
52
+
53
+ - `fluxloop init pytest-template [project_root]` creates `tests/test_fluxloop_smoke.py`, already wired to the new `fluxloop_runner` fixture.
54
+ - Fixtures live in `fluxloop_cli.testing.pytest_plugin` and return a `FluxLoopTestResult`, so you can assert on `total_runs`, `success_rate`, or call `require_success()`.
55
+ - Full guide + CI example: see `docs/guides/pytest_bridge.md` (includes GitHub Actions workflow at `examples/ci/fluxloop_pytest.yml`).
56
+ - Typical workflow:
57
+ 1. `pip install -e packages/cli[dev]`
58
+ 2. `fluxloop init pytest-template .`
59
+ 3. `pytest -k fluxloop_smoke --maxfail=1`
60
+
50
61
  ## Evaluation Workflow
51
62
 
52
63
  Evaluation now follows a two-step process so that multi-turn context is preserved:
53
64
 
54
65
  1. `fluxloop run experiment` – produce `trace_summary.jsonl` (and optionally `observations.jsonl`).
55
66
  2. `fluxloop parse experiment <experiment_dir>` – generate markdown summaries and a structured artifact at `per_trace_analysis/per_trace.jsonl`.
56
- 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`).
57
68
 
58
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`.
59
70
 
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.29"
5
+ __version__ = "0.2.31"
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,188 @@
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
+ report_path = asyncio.run(pipeline.run(trace_summaries))
185
+ console.print(f"\n✅ Report ready: [bold cyan]{report_path}[/bold cyan]")
186
+
187
+
188
+
@@ -18,6 +18,7 @@ from ..templates import (
18
18
  create_sample_agent,
19
19
  create_gitignore,
20
20
  create_env_file,
21
+ create_pytest_bridge_template,
21
22
  )
22
23
  from ..project_paths import resolve_root_dir, resolve_project_dir
23
24
  from ..constants import (
@@ -187,10 +188,77 @@ def project(
187
188
  console.print("4. Generate inputs: [green]fluxloop generate inputs[/green]")
188
189
  console.print("5. Run the experiment: [green]fluxloop run experiment[/green]")
189
190
  console.print("6. Parse outputs: [green]fluxloop parse experiment[/green]")
190
- 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]")
191
192
  console.print("8. Diagnose environment anytime: [green]fluxloop doctor[/green]")
192
193
 
193
194
 
195
+ @app.command("pytest-template")
196
+ def pytest_template(
197
+ project_root: Path = typer.Argument(
198
+ Path.cwd(),
199
+ help="Project root containing configs/ or setting.yaml",
200
+ ),
201
+ tests_dir: str = typer.Option(
202
+ "tests",
203
+ "--tests-dir",
204
+ help="Directory (relative to project root) where tests live",
205
+ ),
206
+ filename: str = typer.Option(
207
+ "test_fluxloop_smoke.py",
208
+ "--filename",
209
+ help="Test file name to create inside the tests directory",
210
+ ),
211
+ force: bool = typer.Option(
212
+ False,
213
+ "--force",
214
+ help="Overwrite existing template without confirmation",
215
+ ),
216
+ ) -> None:
217
+ """
218
+ Scaffold a pytest smoke test that uses the FluxLoop runner fixtures.
219
+ """
220
+
221
+ root_path = project_root.expanduser().resolve()
222
+ if not root_path.exists():
223
+ console.print(f"[red]Error:[/red] Project root {root_path} does not exist.")
224
+ raise typer.Exit(1)
225
+
226
+ tests_path = (root_path / tests_dir).resolve()
227
+ tests_path.mkdir(parents=True, exist_ok=True)
228
+
229
+ target_file = tests_path / filename
230
+
231
+ if target_file.exists() and not force:
232
+ if not Confirm.ask(
233
+ f"{target_file} already exists. Overwrite?",
234
+ default=False,
235
+ ):
236
+ raise typer.Abort()
237
+
238
+ configs_sim = root_path / CONFIG_DIRECTORY_NAME / CONFIG_SECTION_FILENAMES["simulation"]
239
+ legacy_config = root_path / "setting.yaml"
240
+
241
+ if configs_sim.exists():
242
+ relative_config = configs_sim.relative_to(root_path).as_posix()
243
+ elif legacy_config.exists():
244
+ relative_config = legacy_config.relative_to(root_path).as_posix()
245
+ else:
246
+ # Fall back to configs/simulation.yaml even if it does not exist yet
247
+ relative_config = (CONFIG_DIRECTORY_NAME + "/" + CONFIG_SECTION_FILENAMES["simulation"])
248
+ console.print(
249
+ "[yellow]Warning:[/yellow] Could not find configs/simulation.yaml or setting.yaml. "
250
+ "Template will reference the default simulation path."
251
+ )
252
+
253
+ template_content = create_pytest_bridge_template(relative_config)
254
+ target_file.write_text(template_content, encoding="utf-8")
255
+
256
+ console.print(
257
+ f"[green]✓[/green] Pytest template created at [cyan]{target_file}[/cyan]. "
258
+ "Run [bold]pytest -k fluxloop_smoke[/bold] to execute the sample test."
259
+ )
260
+
261
+
194
262
  @app.command()
195
263
  def agent(
196
264
  name: str = typer.Argument(
@@ -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
@@ -118,6 +117,12 @@ def experiment(
118
117
  "--display/--no-display",
119
118
  help="Show rich console output (disable for plain log streaming)",
120
119
  ),
120
+ yes: bool = typer.Option(
121
+ False,
122
+ "--yes",
123
+ "-y",
124
+ help="Skip confirmation prompt and run immediately",
125
+ ),
121
126
  ):
122
127
  """
123
128
  Run an experiment based on configuration file.
@@ -248,7 +253,11 @@ def experiment(
248
253
  f"\nThis will execute {total_runs} runs."
249
254
  )
250
255
 
251
- if not typer.confirm("Continue?"):
256
+ if yes:
257
+ proceed = True
258
+ else:
259
+ proceed = typer.confirm("Continue?")
260
+ if not proceed:
252
261
  raise typer.Abort()
253
262
 
254
263
  # Create runner
@@ -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