fluxloop-cli 0.2.20__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.20 → fluxloop_cli-0.2.21}/PKG-INFO +1 -1
  2. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/__init__.py +1 -1
  3. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/evaluate.py +16 -0
  4. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/init.py +8 -4
  5. fluxloop_cli-0.2.21/fluxloop_cli/environment.py +71 -0
  6. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/config.py +15 -1
  7. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/reporting/html.py +13 -13
  8. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/runner.py +10 -13
  9. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/PKG-INFO +1 -1
  10. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/SOURCES.txt +1 -0
  11. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/pyproject.toml +1 -1
  12. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_evaluate_command.py +48 -0
  13. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/README.md +0 -0
  14. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/arg_binder.py +0 -0
  15. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/__init__.py +0 -0
  16. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/config.py +0 -0
  17. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/doctor.py +0 -0
  18. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/generate.py +0 -0
  19. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/parse.py +0 -0
  20. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/record.py +0 -0
  21. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/run.py +0 -0
  22. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/commands/status.py +0 -0
  23. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/config_loader.py +0 -0
  24. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/config_schema.py +0 -0
  25. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/constants.py +0 -0
  26. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/__init__.py +0 -0
  27. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
  28. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
  29. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/core.py +0 -0
  30. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
  31. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
  32. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/engine/success.py +0 -0
  33. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/llm.py +0 -0
  34. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
  35. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/evaluation/rules.py +0 -0
  36. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/input_generator.py +0 -0
  37. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/llm_generator.py +0 -0
  38. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/main.py +0 -0
  39. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/project_paths.py +0 -0
  40. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/target_loader.py +0 -0
  41. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/templates.py +0 -0
  42. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli/validators.py +0 -0
  43. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  44. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  45. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/requires.txt +0 -0
  46. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/fluxloop_cli.egg-info/top_level.txt +0 -0
  47. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/setup.cfg +0 -0
  48. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_analysis_recommendations.py +0 -0
  49. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_arg_binder.py +0 -0
  50. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_config_command.py +0 -0
  51. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_evaluation_llm.py +0 -0
  52. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_input_generator.py +0 -0
  53. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_prompt_library.py +0 -0
  54. {fluxloop_cli-0.2.20 → fluxloop_cli-0.2.21}/tests/test_success_criteria.py +0 -0
  55. {fluxloop_cli-0.2.20 → 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.20
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.20"
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
@@ -179,12 +179,16 @@ def project(
179
179
  # Show next steps
180
180
  console.print("\n[bold]Next steps:[/bold]")
181
181
  console.print("1. Review configs in [cyan]configs/[/cyan] (project/input/simulation/evaluation)")
182
- 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]")
183
183
  if with_example:
184
- 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]")
185
185
  else:
186
- console.print("3. Add your agent code")
187
- 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]")
188
192
 
189
193
 
190
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.20
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.20"
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