fluxloop-cli 0.2.28__tar.gz → 0.2.30__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.28 → fluxloop_cli-0.2.30}/PKG-INFO +12 -1
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/README.md +11 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/__init__.py +1 -1
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/init.py +68 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/run.py +124 -134
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/templates.py +25 -0
- fluxloop_cli-0.2.30/fluxloop_cli/testing/__init__.py +24 -0
- fluxloop_cli-0.2.30/fluxloop_cli/testing/pytest_plugin.py +421 -0
- fluxloop_cli-0.2.30/fluxloop_cli/testing/types.py +188 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli.egg-info/PKG-INFO +12 -1
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli.egg-info/SOURCES.txt +7 -1
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/pyproject.toml +1 -1
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_evaluation_llm.py +1 -1
- fluxloop_cli-0.2.30/tests/test_init_pytest_template.py +62 -0
- fluxloop_cli-0.2.30/tests/test_pytest_plugin.py +103 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_run_command.py +2 -4
- fluxloop_cli-0.2.30/tests/test_testing_types.py +66 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/arg_binder.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/__init__.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/config.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/doctor.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/evaluate.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/generate.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/parse.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/record.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/commands/status.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/config_loader.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/config_schema.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/constants.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/conversation_supervisor.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/environment.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/__init__.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/artifacts.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/config.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/core.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/reporting/html.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/engine/success.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/llm.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/prompts/base.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/prompts/information_completeness.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/prompts/intent_recognition.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/prompts/response_clarity.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/prompts/response_consistency.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/evaluation/rules.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/input_generator.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/llm_generator.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/main.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/project_paths.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/runner.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/target_loader.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli/validators.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli.egg-info/entry_points.txt +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli.egg-info/requires.txt +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/fluxloop_cli.egg-info/top_level.txt +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/setup.cfg +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_analysis_recommendations.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_arg_binder.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_config_command.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_conversation_supervisor.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_evaluate_command.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_input_generator.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_prompt_library.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_runner_multi_turn.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/tests/test_success_criteria.py +0 -0
- {fluxloop_cli-0.2.28 → fluxloop_cli-0.2.30}/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.
|
|
3
|
+
Version: 0.2.30
|
|
4
4
|
Summary: FluxLoop CLI for running agent simulations
|
|
5
5
|
Author-email: FluxLoop Team <team@fluxloop.dev>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -71,6 +71,7 @@ The legacy `setting.yaml` is still supported, but new projects created with
|
|
|
71
71
|
- `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
|
|
72
72
|
- `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
|
|
73
73
|
- `fluxloop doctor` – summarize Python, FluxLoop CLI/MCP, and MCP index state for the active environment
|
|
74
|
+
- `--yes/-y` (for `fluxloop run experiment`) – skip the interactive confirmation prompt, ideal for CI and the Pytest bridge
|
|
74
75
|
|
|
75
76
|
### Multi-turn supervisor options
|
|
76
77
|
|
|
@@ -88,6 +89,16 @@ These flags override the values in `configs/simulation.yaml` (`multi_turn` block
|
|
|
88
89
|
|
|
89
90
|
Run `fluxloop --help` or `fluxloop <command> --help` for more detail.
|
|
90
91
|
|
|
92
|
+
## Pytest Bridge (0.2.29+)
|
|
93
|
+
|
|
94
|
+
- `fluxloop init pytest-template [project_root]` creates `tests/test_fluxloop_smoke.py`, already wired to the new `fluxloop_runner` fixture.
|
|
95
|
+
- 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()`.
|
|
96
|
+
- Full guide + CI example: see `docs/guides/pytest_bridge.md` (includes GitHub Actions workflow at `examples/ci/fluxloop_pytest.yml`).
|
|
97
|
+
- Typical workflow:
|
|
98
|
+
1. `pip install -e packages/cli[dev]`
|
|
99
|
+
2. `fluxloop init pytest-template .`
|
|
100
|
+
3. `pytest -k fluxloop_smoke --maxfail=1`
|
|
101
|
+
|
|
91
102
|
## Evaluation Workflow
|
|
92
103
|
|
|
93
104
|
Evaluation now follows a two-step process so that multi-turn context is preserved:
|
|
@@ -30,6 +30,7 @@ The legacy `setting.yaml` is still supported, but new projects created with
|
|
|
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,6 +48,16 @@ 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:
|
|
@@ -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 (
|
|
@@ -191,6 +192,73 @@ def project(
|
|
|
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(
|
|
@@ -3,10 +3,9 @@ Run command for executing experiments and simulations.
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
-
import json
|
|
7
6
|
import sys
|
|
8
7
|
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
8
|
+
from typing import Callable, Optional
|
|
10
9
|
|
|
11
10
|
import typer
|
|
12
11
|
from rich.console import Console
|
|
@@ -28,16 +27,6 @@ app = typer.Typer()
|
|
|
28
27
|
console = Console()
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
def _emit_progress_event(event: str, **payload: object) -> None:
|
|
32
|
-
"""Emit a structured progress event to stderr for machine consumption."""
|
|
33
|
-
data = {"event": event, **payload}
|
|
34
|
-
try:
|
|
35
|
-
print(json.dumps(data, default=str), file=sys.stderr, flush=True)
|
|
36
|
-
except Exception:
|
|
37
|
-
# Fall back to repr if serialization fails
|
|
38
|
-
print({"event": event, **{k: repr(v) for k, v in payload.items()}}, file=sys.stderr, flush=True)
|
|
39
|
-
|
|
40
|
-
|
|
41
30
|
@app.command()
|
|
42
31
|
def experiment(
|
|
43
32
|
config_file: Path = typer.Option(
|
|
@@ -124,6 +113,17 @@ def experiment(
|
|
|
124
113
|
"--dry-run",
|
|
125
114
|
help="Show what would be run without executing",
|
|
126
115
|
),
|
|
116
|
+
display: bool = typer.Option(
|
|
117
|
+
True,
|
|
118
|
+
"--display/--no-display",
|
|
119
|
+
help="Show rich console output (disable for plain log streaming)",
|
|
120
|
+
),
|
|
121
|
+
yes: bool = typer.Option(
|
|
122
|
+
False,
|
|
123
|
+
"--yes",
|
|
124
|
+
"-y",
|
|
125
|
+
help="Skip confirmation prompt and run immediately",
|
|
126
|
+
),
|
|
127
127
|
):
|
|
128
128
|
"""
|
|
129
129
|
Run an experiment based on configuration file.
|
|
@@ -239,19 +239,8 @@ def experiment(
|
|
|
239
239
|
|
|
240
240
|
console.print(summary)
|
|
241
241
|
|
|
242
|
-
_emit_progress_event(
|
|
243
|
-
"experiment_summary",
|
|
244
|
-
name=config.name,
|
|
245
|
-
iterations=config.iterations,
|
|
246
|
-
personas=len(config.personas),
|
|
247
|
-
total_runs=total_runs,
|
|
248
|
-
multi_turn_enabled=bool(config.multi_turn and config.multi_turn.enabled),
|
|
249
|
-
output_dir=config.output_directory,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
242
|
if dry_run:
|
|
253
243
|
console.print("\n[yellow]Dry run mode - no execution will occur[/yellow]")
|
|
254
|
-
_emit_progress_event("experiment_status", status="dry_run", name=config.name)
|
|
255
244
|
return
|
|
256
245
|
|
|
257
246
|
# Confirm execution
|
|
@@ -265,128 +254,138 @@ def experiment(
|
|
|
265
254
|
f"\nThis will execute {total_runs} runs."
|
|
266
255
|
)
|
|
267
256
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if not typer.confirm("Continue?"):
|
|
275
|
-
_emit_progress_event("experiment_status", status="aborted", reason="user_declined", name=config.name)
|
|
257
|
+
if yes:
|
|
258
|
+
proceed = True
|
|
259
|
+
else:
|
|
260
|
+
proceed = typer.confirm("Continue?")
|
|
261
|
+
if not proceed:
|
|
276
262
|
raise typer.Abort()
|
|
277
263
|
|
|
278
264
|
# Create runner
|
|
279
265
|
# Run experiment with progress tracking
|
|
280
266
|
console.print("\n[bold green]▶️ Starting experiment...[/bold green]\n")
|
|
281
267
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
else None
|
|
302
|
-
)
|
|
303
|
-
turn_task = progress.add_task(
|
|
304
|
-
"[cyan]Turn 0/0",
|
|
305
|
-
total=multi_turn_total or 1,
|
|
306
|
-
visible=False,
|
|
307
|
-
)
|
|
268
|
+
def _execute_with_callbacks(
|
|
269
|
+
progress_callback: Optional[Callable[[], None]],
|
|
270
|
+
turn_progress_callback: Optional[Callable[[int, int, Optional[str]], None]],
|
|
271
|
+
):
|
|
272
|
+
try:
|
|
273
|
+
return asyncio.run(
|
|
274
|
+
runner.run_experiment(
|
|
275
|
+
progress_callback=progress_callback,
|
|
276
|
+
turn_progress_callback=turn_progress_callback,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
except KeyboardInterrupt:
|
|
280
|
+
console.print("\n[yellow]Experiment interrupted by user[/yellow]")
|
|
281
|
+
raise typer.Exit(1)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
console.print(f"\n[red]Error during experiment:[/red] {e}")
|
|
284
|
+
if "--debug" in sys.argv:
|
|
285
|
+
console.print_exception()
|
|
286
|
+
raise typer.Exit(1)
|
|
308
287
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
288
|
+
if display and console.is_terminal:
|
|
289
|
+
with Progress(
|
|
290
|
+
SpinnerColumn(),
|
|
291
|
+
TextColumn("[progress.description]{task.description}"),
|
|
292
|
+
BarColumn(),
|
|
293
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
294
|
+
TimeRemainingColumn(),
|
|
295
|
+
TextColumn("({task.completed} of {task.total})"),
|
|
296
|
+
console=console,
|
|
297
|
+
) as progress:
|
|
298
|
+
main_task = progress.add_task(
|
|
299
|
+
f"Running {config.name}",
|
|
300
|
+
total=total_runs,
|
|
301
|
+
)
|
|
302
|
+
multi_turn_total = (
|
|
303
|
+
config.multi_turn.max_turns
|
|
304
|
+
if config.multi_turn and config.multi_turn.max_turns
|
|
305
|
+
else None
|
|
306
|
+
)
|
|
307
|
+
turn_task = progress.add_task(
|
|
308
|
+
"[cyan]Turn 0/0",
|
|
309
|
+
total=multi_turn_total or 1,
|
|
310
|
+
visible=False,
|
|
318
311
|
)
|
|
319
312
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
313
|
+
def _progress_callback() -> None:
|
|
314
|
+
progress.advance(main_task)
|
|
315
|
+
|
|
316
|
+
def _turn_progress_callback(
|
|
317
|
+
current_turn: int,
|
|
318
|
+
total_turns: int,
|
|
319
|
+
message: Optional[str] = None,
|
|
320
|
+
) -> None:
|
|
321
|
+
total = total_turns or 1
|
|
322
|
+
clamped_turn = max(0, current_turn)
|
|
323
|
+
if message is None:
|
|
324
|
+
completed = min(clamped_turn, total)
|
|
325
|
+
description = f"[cyan]Turn {min(clamped_turn, total)}/{total}: complete"
|
|
326
|
+
progress.update(
|
|
327
|
+
turn_task,
|
|
328
|
+
total=total,
|
|
329
|
+
completed=completed,
|
|
330
|
+
description=description,
|
|
331
|
+
visible=False,
|
|
332
|
+
)
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
if not progress.tasks[turn_task].visible:
|
|
336
|
+
progress.update(turn_task, visible=True)
|
|
337
|
+
|
|
338
|
+
preview = message.replace("\n", " ")
|
|
339
|
+
if len(preview) > 40:
|
|
340
|
+
preview = preview[:37] + "..."
|
|
341
|
+
|
|
342
|
+
completed = max(0, min(clamped_turn - 1, total))
|
|
343
|
+
description = f"[cyan]Turn {min(clamped_turn, total)}/{total}: {preview}"
|
|
330
344
|
progress.update(
|
|
331
345
|
turn_task,
|
|
332
346
|
total=total,
|
|
333
347
|
completed=completed,
|
|
334
348
|
description=description,
|
|
335
|
-
visible=False,
|
|
336
|
-
)
|
|
337
|
-
_emit_progress_event(
|
|
338
|
-
"turn_progress",
|
|
339
|
-
state="complete",
|
|
340
|
-
current=min(clamped_turn, total),
|
|
341
|
-
total=total,
|
|
342
|
-
experiment=config.name,
|
|
343
349
|
)
|
|
344
|
-
return
|
|
345
|
-
|
|
346
|
-
if not progress.tasks[turn_task].visible:
|
|
347
|
-
progress.update(turn_task, visible=True)
|
|
348
350
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
completed = max(0, min(clamped_turn - 1, total))
|
|
354
|
-
description = f"[cyan]Turn {min(clamped_turn, total)}/{total}: {preview}"
|
|
351
|
+
results = _execute_with_callbacks(_progress_callback, _turn_progress_callback)
|
|
352
|
+
else:
|
|
353
|
+
completed_runs = 0
|
|
355
354
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
_emit_progress_event(
|
|
363
|
-
"turn_progress",
|
|
364
|
-
state="in_progress",
|
|
365
|
-
current=clamped_turn,
|
|
366
|
-
total=total,
|
|
367
|
-
preview=preview,
|
|
368
|
-
experiment=config.name,
|
|
355
|
+
def _progress_callback() -> None:
|
|
356
|
+
nonlocal completed_runs
|
|
357
|
+
completed_runs += 1
|
|
358
|
+
print(
|
|
359
|
+
f"[fluxloop] run progress {completed_runs}/{total_runs}",
|
|
360
|
+
flush=True,
|
|
369
361
|
)
|
|
370
362
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
363
|
+
def _turn_progress_callback(
|
|
364
|
+
current_turn: int,
|
|
365
|
+
total_turns: int,
|
|
366
|
+
message: Optional[str] = None,
|
|
367
|
+
) -> None:
|
|
368
|
+
total = total_turns or 1
|
|
369
|
+
clamped_turn = max(0, current_turn)
|
|
370
|
+
if message:
|
|
371
|
+
preview = message.replace("\n", " ")
|
|
372
|
+
if len(preview) > 80:
|
|
373
|
+
preview = preview[:77] + "..."
|
|
374
|
+
print(
|
|
375
|
+
f"[fluxloop] turn progress {min(clamped_turn, total)}/{total}: {preview}",
|
|
376
|
+
flush=True,
|
|
377
377
|
)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
raise typer.Exit(1)
|
|
378
|
+
else:
|
|
379
|
+
print(
|
|
380
|
+
f"[fluxloop] turn progress {min(clamped_turn, total)}/{total}: complete",
|
|
381
|
+
flush=True,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
print(
|
|
385
|
+
f"[fluxloop] running {config.name} ({total_runs} total runs)",
|
|
386
|
+
flush=True,
|
|
387
|
+
)
|
|
388
|
+
results = _execute_with_callbacks(_progress_callback, _turn_progress_callback)
|
|
390
389
|
|
|
391
390
|
config.set_resolved_input_count(results.get("input_count", config.get_input_count()))
|
|
392
391
|
|
|
@@ -399,15 +398,6 @@ def experiment(
|
|
|
399
398
|
)
|
|
400
399
|
|
|
401
400
|
_display_results(results)
|
|
402
|
-
_emit_progress_event(
|
|
403
|
-
"experiment_status",
|
|
404
|
-
status="completed",
|
|
405
|
-
name=config.name,
|
|
406
|
-
total_runs=results.get("total_runs", total_runs),
|
|
407
|
-
successful=results.get("successful"),
|
|
408
|
-
failed=results.get("failed"),
|
|
409
|
-
success_rate=results.get("success_rate"),
|
|
410
|
-
)
|
|
411
401
|
|
|
412
402
|
|
|
413
403
|
@app.command()
|
|
@@ -429,6 +429,31 @@ def create_evaluation_config() -> str:
|
|
|
429
429
|
).strip() + "\n"
|
|
430
430
|
|
|
431
431
|
|
|
432
|
+
def create_pytest_bridge_template(config_relative_path: str) -> str:
|
|
433
|
+
"""Return a ready-to-run pytest smoke test referencing the bridge fixtures."""
|
|
434
|
+
|
|
435
|
+
return dedent(
|
|
436
|
+
f"""
|
|
437
|
+
\"\"\"FluxLoop Pytest smoke test generated via `fluxloop init pytest-template`.\"\"\"
|
|
438
|
+
|
|
439
|
+
from pathlib import Path
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
443
|
+
SIMULATION_CONFIG = PROJECT_ROOT / "{config_relative_path}"
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_fluxloop_smoke(fluxloop_runner):
|
|
447
|
+
result = fluxloop_runner(
|
|
448
|
+
project_root=PROJECT_ROOT,
|
|
449
|
+
simulation_config=SIMULATION_CONFIG,
|
|
450
|
+
env={{"PYTHONPATH": str(PROJECT_ROOT)}},
|
|
451
|
+
)
|
|
452
|
+
result.require_success(label="fluxloop smoke")
|
|
453
|
+
"""
|
|
454
|
+
).strip() + "\n"
|
|
455
|
+
|
|
456
|
+
|
|
432
457
|
def create_sample_agent() -> str:
|
|
433
458
|
"""Create a sample agent implementation."""
|
|
434
459
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Testing utilities interface definitions for FluxLoop Pytest bridge.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .types import (
|
|
6
|
+
DEFAULT_SCENARIOS,
|
|
7
|
+
FluxLoopRunnerMode,
|
|
8
|
+
FluxLoopRunnerRequest,
|
|
9
|
+
FluxLoopRunnerOverrides,
|
|
10
|
+
FluxLoopTestError,
|
|
11
|
+
FluxLoopTestResult,
|
|
12
|
+
FluxLoopTestScenario,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"DEFAULT_SCENARIOS",
|
|
17
|
+
"FluxLoopRunnerMode",
|
|
18
|
+
"FluxLoopRunnerOverrides",
|
|
19
|
+
"FluxLoopRunnerRequest",
|
|
20
|
+
"FluxLoopTestError",
|
|
21
|
+
"FluxLoopTestResult",
|
|
22
|
+
"FluxLoopTestScenario",
|
|
23
|
+
]
|
|
24
|
+
|