fluxloop-cli 0.2.19__tar.gz → 0.2.21__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 (55) hide show
  1. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/PKG-INFO +1 -1
  2. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/__init__.py +1 -1
  3. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/evaluate.py +16 -0
  4. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/init.py +10 -22
  5. fluxloop_cli-0.2.21/fluxloop_cli/environment.py +71 -0
  6. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/config.py +15 -1
  7. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/reporting/html.py +13 -13
  8. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/runner.py +10 -13
  9. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/templates.py +4 -3
  10. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/PKG-INFO +1 -1
  11. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/SOURCES.txt +1 -0
  12. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/pyproject.toml +1 -1
  13. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_evaluate_command.py +48 -0
  14. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/README.md +0 -0
  15. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/arg_binder.py +0 -0
  16. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/__init__.py +0 -0
  17. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/config.py +0 -0
  18. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/doctor.py +0 -0
  19. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/generate.py +0 -0
  20. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/parse.py +0 -0
  21. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/record.py +0 -0
  22. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/run.py +0 -0
  23. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/status.py +0 -0
  24. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/config_loader.py +0 -0
  25. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/config_schema.py +0 -0
  26. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/constants.py +0 -0
  27. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/__init__.py +0 -0
  28. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
  29. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
  30. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/core.py +0 -0
  31. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
  32. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
  33. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/success.py +0 -0
  34. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/llm.py +0 -0
  35. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
  36. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/rules.py +0 -0
  37. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/input_generator.py +0 -0
  38. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/llm_generator.py +0 -0
  39. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/main.py +0 -0
  40. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/project_paths.py +0 -0
  41. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/target_loader.py +0 -0
  42. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli/validators.py +0 -0
  43. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  44. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  45. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/requires.txt +0 -0
  46. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/top_level.txt +0 -0
  47. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/setup.cfg +0 -0
  48. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_analysis_recommendations.py +0 -0
  49. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_arg_binder.py +0 -0
  50. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_config_command.py +0 -0
  51. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_evaluation_llm.py +0 -0
  52. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_input_generator.py +0 -0
  53. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_prompt_library.py +0 -0
  54. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/tests/test_success_criteria.py +0 -0
  55. {fluxloop_cli-0.2.19 → fluxloop_cli-0.2.21}/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.19
3
+ Version: 0.2.21
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.19"
5
+ __version__ = "0.2.21"
6
6
 
7
7
  from .main import app
8
8
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
8
9
  import typer
9
10
  from rich.console import Console
10
11
 
12
+ from ..environment import load_env_chain
11
13
  from ..evaluation import EvaluationOptions, load_evaluation_config, run_evaluation
12
14
 
13
15
  console = Console()
@@ -134,6 +136,20 @@ def experiment(
134
136
  except Exception as exc: # noqa: BLE001
135
137
  raise typer.BadParameter(f"Failed to load evaluation config: {exc}") from exc
136
138
 
139
+ def _log_env_error(path: Path, exc: Exception) -> None:
140
+ console.log(
141
+ f"[yellow]Warning:[/yellow] Failed to load environment from {path}: {exc}"
142
+ )
143
+
144
+ load_env_chain(
145
+ evaluation_config.get_source_dir(),
146
+ refresh_config=True,
147
+ on_error=_log_env_error,
148
+ )
149
+
150
+ if llm_api_key is None:
151
+ llm_api_key = os.getenv("FLUXLOOP_LLM_API_KEY") or os.getenv("OPENAI_API_KEY")
152
+
137
153
  output_dir = output
138
154
  if not output_dir.is_absolute():
139
155
  output_dir = resolved_experiment_dir / output_dir
@@ -137,27 +137,11 @@ def project(
137
137
  section_path = section_paths[key]
138
138
  section_path.write_text(content)
139
139
 
140
- # Create project .env (unified)
141
- root_env_file = root_dir / ".env"
140
+ # Create project .env (single source of truth)
142
141
  console.print("🔐 Creating project .env...")
143
142
  recordings_dir = project_path / "recordings"
144
143
  recordings_dir.mkdir(exist_ok=True)
145
-
146
- # If a root .env exists, seed project .env with its contents so project can override locally
147
- if root_env_file.exists():
148
- try:
149
- root_contents = root_env_file.read_text()
150
- except Exception:
151
- root_contents = ""
152
- merged = (
153
- "# Project .env (seeded from FluxLoop root .env)\n"
154
- + (root_contents if root_contents.endswith("\n") else root_contents + "\n")
155
- + "\n# Project-specific overrides (take precedence)\n"
156
- )
157
- env_file.write_text(merged)
158
- console.print("[dim]Seeded from root .env (project overrides take precedence).[/dim]")
159
- else:
160
- env_file.write_text(create_env_file())
144
+ env_file.write_text(create_env_file())
161
145
 
162
146
  # Update .gitignore
163
147
  if not gitignore_file.exists():
@@ -195,12 +179,16 @@ def project(
195
179
  # Show next steps
196
180
  console.print("\n[bold]Next steps:[/bold]")
197
181
  console.print("1. Review configs in [cyan]configs/[/cyan] (project/input/simulation/evaluation)")
198
- console.print("2. Set up environment variables in [cyan].env[/cyan]")
182
+ console.print("2. Configure secrets via [cyan].env[/cyan] or [green]fluxloop config set-llm[/green]")
199
183
  if with_example:
200
- console.print("3. Try running: [green]fluxloop run experiment[/green]")
184
+ console.print("3. Customize the sample agent in [cyan]examples/simple_agent.py[/cyan]")
201
185
  else:
202
- console.print("3. Add your agent code")
203
- console.print("4. Run: [green]fluxloop run experiment[/green]")
186
+ console.print("3. Create your agent: [green]fluxloop init agent <name>[/green]")
187
+ console.print("4. Generate inputs: [green]fluxloop generate inputs[/green]")
188
+ console.print("5. Run the experiment: [green]fluxloop run experiment[/green]")
189
+ 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("8. Diagnose environment anytime: [green]fluxloop doctor[/green]")
204
192
 
205
193
 
206
194
  @app.command()
@@ -0,0 +1,71 @@
1
+ """
2
+ Utilities for applying project environment settings across CLI commands.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import Callable, Iterable, Optional, Set
9
+
10
+ import fluxloop
11
+
12
+ EnvErrorHandler = Callable[[Path, Exception], None]
13
+
14
+
15
+ def load_env_chain(
16
+ source_dir: Optional[Path],
17
+ *,
18
+ additional_paths: Optional[Iterable[Path]] = None,
19
+ refresh_config: bool = True,
20
+ on_error: Optional[EnvErrorHandler] = None,
21
+ ) -> None:
22
+ """
23
+ Load environment variables from `.env` files related to a project.
24
+
25
+ Parameters
26
+ ----------
27
+ source_dir:
28
+ Directory associated with the current config (usually the config file's parent).
29
+ additional_paths:
30
+ Extra `.env` files to consider (applied after the standard chain).
31
+ refresh_config:
32
+ Whether to refresh the fluxloop config after loading.
33
+ on_error:
34
+ Optional callback invoked when loading a path fails.
35
+ """
36
+
37
+ if source_dir is None:
38
+ candidates: Iterable[Path] = additional_paths or []
39
+ else:
40
+ resolved_source = source_dir.resolve()
41
+ parents: list[Path] = []
42
+ parent = resolved_source.parent
43
+ if parent != resolved_source:
44
+ parents.append(parent / ".env")
45
+ parents.append(resolved_source / ".env")
46
+
47
+ extra = list(additional_paths or [])
48
+ candidates = [*parents, *extra]
49
+
50
+ seen: Set[Path] = set()
51
+ for path in candidates:
52
+ # Avoid duplicate loads and normalize the path
53
+ resolved = path.resolve()
54
+ if resolved in seen:
55
+ continue
56
+ seen.add(resolved)
57
+
58
+ if not resolved.exists():
59
+ continue
60
+
61
+ try:
62
+ fluxloop.load_env(resolved, override=True, refresh_config=refresh_config)
63
+ except Exception as exc: # noqa: BLE001
64
+ if on_error is None:
65
+ raise
66
+ on_error(resolved, exc)
67
+
68
+
69
+ __all__ = ["load_env_chain", "EnvErrorHandler"]
70
+
71
+
@@ -230,6 +230,14 @@ class EvaluationConfig:
230
230
  report: ReportConfig = field(default_factory=ReportConfig)
231
231
  advanced: AdvancedConfig = field(default_factory=AdvancedConfig)
232
232
 
233
+ def set_source_dir(self, source_dir: Path) -> None:
234
+ """Remember the directory where this config file was loaded from."""
235
+ self._source_dir = source_dir
236
+
237
+ def get_source_dir(self) -> Optional[Path]:
238
+ """Return the directory where the config file was loaded from."""
239
+ return getattr(self, "_source_dir", None)
240
+
233
241
 
234
242
  def _load_yaml(path: Path) -> Dict[str, Any]:
235
243
  if not path.exists():
@@ -704,7 +712,9 @@ def load_evaluation_config(path: Path) -> EvaluationConfig:
704
712
  report = _parse_report(data.get("report"))
705
713
  advanced = _parse_advanced(data.get("advanced"))
706
714
 
707
- return EvaluationConfig(
715
+ config_dir = path.parent.resolve()
716
+
717
+ config = EvaluationConfig(
708
718
  evaluators=evaluators,
709
719
  aggregate=aggregate,
710
720
  limits=limits,
@@ -715,4 +725,8 @@ def load_evaluation_config(path: Path) -> EvaluationConfig:
715
725
  advanced=advanced,
716
726
  )
717
727
 
728
+ config.set_source_dir(config_dir)
729
+
730
+ return config
731
+
718
732
 
@@ -48,8 +48,8 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
48
48
  <main class="max-w-6xl mx-auto p-8 space-y-6">
49
49
  <header class="space-y-6">
50
50
  <div>
51
- <h1 class="text-3xl font-bold">[[TITLE]]</h1>
52
- <p class="text-sm text-slate-300">Generated at [[DATE]]</p>
51
+ <h1 class="text-3xl font-bold">[[TITLE]]</h1>
52
+ <p class="text-sm text-slate-300">Generated at [[DATE]]</p>
53
53
  </div>
54
54
  <nav class="flex flex-wrap gap-2" role="tablist" aria-label="Report sections">
55
55
  <button class="tab-button px-4 py-2 rounded-full bg-sky-500/20 border border-sky-400 text-sky-200 font-semibold" data-tab-button="summary">Executive Summary</button>
@@ -203,14 +203,14 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
203
203
  });
204
204
  }
205
205
  container.innerHTML = cards
206
- .map(
206
+ .map(
207
207
  (card) => `
208
208
  <div class="bg-slate-900 rounded-xl p-4 border border-slate-800 shadow-inner">
209
209
  <p class="text-slate-400 text-xs uppercase tracking-wide">${card.label}</p>
210
210
  <p class="text-2xl font-semibold mt-2">${card.value}</p>
211
- </div>
212
- `
213
- )
211
+ </div>
212
+ `
213
+ )
214
214
  .join("");
215
215
  }
216
216
 
@@ -236,21 +236,21 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
236
236
  if (!payload) return;
237
237
  const checks = Object.entries(payload);
238
238
  if (!checks.length) return;
239
- const sectionTitle = section.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
239
+ const sectionTitle = section.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
240
240
  const wrapper = document.createElement("div");
241
241
  wrapper.className = "bg-slate-900 border border-slate-800 rounded-xl p-4 space-y-2";
242
242
  wrapper.innerHTML = `<h3 class="text-lg font-semibold">${sectionTitle}</h3>`;
243
- const list = document.createElement("ul");
244
- list.className = "space-y-1 text-sm text-slate-300";
245
- for (const [name, details] of checks) {
243
+ const list = document.createElement("ul");
244
+ list.className = "space-y-1 text-sm text-slate-300";
245
+ for (const [name, details] of checks) {
246
246
  const prettyName = toTitleCase(name);
247
- const status = details.met === true ? "✅ Met" : details.met === false ? "❌ Not met" : "⚪️ Not evaluated";
247
+ const status = details.met === true ? "✅ Met" : details.met === false ? "❌ Not met" : "⚪️ Not evaluated";
248
248
  const meta = { ...details };
249
249
  delete meta.met;
250
250
  const extra =
251
251
  Object.keys(meta).length > 0 ? `<span class="text-slate-400"> ${JSON.stringify(meta)}</span>` : "";
252
252
  list.innerHTML += `<li>${status} · ${prettyName}${extra}</li>`;
253
- }
253
+ }
254
254
  wrapper.appendChild(list);
255
255
  container.appendChild(wrapper);
256
256
  });
@@ -453,7 +453,7 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
453
453
  const shown = filtered.length;
454
454
  const suffix = total === 1 ? "" : "s";
455
455
  hint.textContent = `${shown} of ${total} trace${suffix} shown`;
456
- }
456
+ }
457
457
  }
458
458
 
459
459
  initTabs();
@@ -21,6 +21,7 @@ from fluxloop.buffer import EventBuffer
21
21
  from fluxloop.schemas import ExperimentConfig, PersonaConfig
22
22
  from rich.console import Console
23
23
 
24
+ from .environment import load_env_chain
24
25
  from .target_loader import TargetLoader
25
26
  from .arg_binder import ArgBinder
26
27
 
@@ -97,21 +98,17 @@ class ExperimentRunner:
97
98
  """Load environment variables from .env and runner settings."""
98
99
 
99
100
  source_dir = self.config.get_source_dir()
100
- env_candidates: List[Path] = []
101
101
 
102
- # Load parent (root) .env first, then project .env so project overrides take precedence
103
- if source_dir:
104
- parent = source_dir.parent
105
- if parent != source_dir:
106
- env_candidates.append(parent / ".env")
107
- env_candidates.append(source_dir / ".env")
102
+ def _log_env_error(path: Path, exc: Exception) -> None:
103
+ console.log(
104
+ f"[yellow]Warning:[/yellow] Failed to load environment from {path}: {exc}"
105
+ )
108
106
 
109
- for candidate in env_candidates:
110
- if candidate.exists():
111
- try:
112
- fluxloop.load_env(candidate, override=True, refresh_config=True)
113
- except Exception:
114
- console.log(f"[yellow]Warning:[/yellow] Failed to load environment from {candidate}")
107
+ load_env_chain(
108
+ source_dir,
109
+ refresh_config=True,
110
+ on_error=_log_env_error,
111
+ )
115
112
 
116
113
  env_vars = getattr(self.config.runner, "environment_vars", {}) or {}
117
114
  for key, value in env_vars.items():
@@ -522,15 +522,16 @@ def create_env_file() -> str:
522
522
  FLUXLOOP_SAMPLE_RATE=1.0
523
523
  # Argument Recording (global toggle)
524
524
  FLUXLOOP_RECORD_ARGS=false
525
- FLUXLOOP_RECORDING_FILE=recordings/args_recording.jsonl
525
+ FLUXLOOP_RECORDING_FILE=
526
+ # Example: recordings/args_recording.jsonl (project-relative) or absolute path
526
527
 
527
528
  # Service Configuration
528
529
  FLUXLOOP_SERVICE_NAME=my-agent
529
530
  FLUXLOOP_ENVIRONMENT=development
530
531
 
531
532
  # LLM API Keys (if needed)
532
- # OPENAI_API_KEY=sk-...
533
- # ANTHROPIC_API_KEY=sk-ant-...
533
+ OPENAI_API_KEY=
534
+ # ANTHROPIC_API_KEY=
534
535
 
535
536
  # Other Configuration
536
537
  # Add your custom environment variables here
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.19
3
+ Version: 0.2.21
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -5,6 +5,7 @@ fluxloop_cli/arg_binder.py
5
5
  fluxloop_cli/config_loader.py
6
6
  fluxloop_cli/config_schema.py
7
7
  fluxloop_cli/constants.py
8
+ fluxloop_cli/environment.py
8
9
  fluxloop_cli/input_generator.py
9
10
  fluxloop_cli/llm_generator.py
10
11
  fluxloop_cli/main.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fluxloop-cli"
7
- version = "0.2.19"
7
+ version = "0.2.21"
8
8
  description = "FluxLoop CLI for running agent simulations"
9
9
  authors = [
10
10
  {name = "FluxLoop Team", email = "team@fluxloop.dev"}
@@ -333,3 +333,51 @@ def test_evaluate_phase2_extended_outputs(tmp_path: Path) -> None:
333
333
  assert "outliers" in performance_analysis and "trends" in performance_analysis
334
334
  comparison = analysis["comparison"]
335
335
  assert "baseline_path" in comparison and comparison["baseline_path"].endswith("baseline_summary.json")
336
+
337
+
338
+ def test_evaluate_loads_env_for_llm(tmp_path: Path, monkeypatch) -> None:
339
+ project_dir = tmp_path
340
+ configs_dir = project_dir / "configs"
341
+ configs_dir.mkdir()
342
+
343
+ env_path = project_dir / ".env"
344
+ env_path.write_text("FLUXLOOP_LLM_API_KEY=sk-test-key\n", encoding="utf-8")
345
+
346
+ eval_config = configs_dir / "evaluation.yaml"
347
+ _write_eval_config(eval_config, include_llm=True)
348
+
349
+ experiment_dir = project_dir / "experiments" / "run_1"
350
+ experiment_dir.mkdir(parents=True, exist_ok=True)
351
+ _write_trace_summary(experiment_dir / "trace_summary.jsonl")
352
+
353
+ captured: dict[str, object] = {}
354
+
355
+ def fake_run_evaluation(exp_dir, config, options):
356
+ captured["llm_api_key"] = options.llm_api_key
357
+ captured["sample_rate"] = options.sample_rate
358
+ return {}
359
+
360
+ monkeypatch.delenv("FLUXLOOP_LLM_API_KEY", raising=False)
361
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
362
+ monkeypatch.setattr(
363
+ "fluxloop_cli.commands.evaluate.run_evaluation", fake_run_evaluation
364
+ )
365
+
366
+ result = runner.invoke(
367
+ cli_main.app,
368
+ [
369
+ "evaluate",
370
+ "experiment",
371
+ str(experiment_dir),
372
+ "--config",
373
+ str(eval_config),
374
+ "--output",
375
+ "evaluation",
376
+ ],
377
+ )
378
+
379
+ monkeypatch.delenv("FLUXLOOP_LLM_API_KEY", raising=False)
380
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
381
+
382
+ assert result.exit_code == 0, result.output
383
+ assert captured["llm_api_key"] == "sk-test-key"
File without changes
File without changes