fluxloop-cli 0.2.18__tar.gz → 0.2.20__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.18 → fluxloop_cli-0.2.20}/PKG-INFO +2 -2
  2. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/README.md +1 -1
  3. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/__init__.py +1 -1
  4. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/init.py +2 -18
  5. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/config.py +11 -0
  6. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/analysis.py +139 -0
  7. fluxloop_cli-0.2.20/fluxloop_cli/evaluation/engine/reporting/html.py +523 -0
  8. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/reporting/markdown.py +10 -0
  9. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/llm.py +55 -2
  10. fluxloop_cli-0.2.20/fluxloop_cli/evaluation/prompts/__init__.py +182 -0
  11. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/templates.py +210 -209
  12. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/PKG-INFO +2 -2
  13. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/SOURCES.txt +5 -0
  14. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/pyproject.toml +1 -1
  15. fluxloop_cli-0.2.20/tests/test_analysis_recommendations.py +82 -0
  16. fluxloop_cli-0.2.20/tests/test_evaluation_llm.py +92 -0
  17. fluxloop_cli-0.2.20/tests/test_prompt_library.py +29 -0
  18. fluxloop_cli-0.2.20/tests/test_success_criteria.py +54 -0
  19. fluxloop_cli-0.2.18/fluxloop_cli/evaluation/engine/reporting/html.py +0 -257
  20. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/arg_binder.py +0 -0
  21. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/__init__.py +0 -0
  22. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/config.py +0 -0
  23. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/doctor.py +0 -0
  24. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/evaluate.py +0 -0
  25. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/generate.py +0 -0
  26. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/parse.py +0 -0
  27. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/record.py +0 -0
  28. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/run.py +0 -0
  29. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/status.py +0 -0
  30. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/config_loader.py +0 -0
  31. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/config_schema.py +0 -0
  32. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/constants.py +0 -0
  33. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/__init__.py +0 -0
  34. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
  35. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/core.py +0 -0
  36. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
  37. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/success.py +0 -0
  38. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/rules.py +0 -0
  39. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/input_generator.py +0 -0
  40. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/llm_generator.py +0 -0
  41. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/main.py +0 -0
  42. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/project_paths.py +0 -0
  43. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/runner.py +0 -0
  44. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/target_loader.py +0 -0
  45. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/validators.py +0 -0
  46. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  47. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  48. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/requires.txt +0 -0
  49. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/top_level.txt +0 -0
  50. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/setup.cfg +0 -0
  51. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_arg_binder.py +0 -0
  52. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_config_command.py +0 -0
  53. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_evaluate_command.py +0 -0
  54. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_input_generator.py +0 -0
  55. {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/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.18
3
+ Version: 0.2.20
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -66,7 +66,7 @@ The legacy `setting.yaml` is still supported, but new projects created with
66
66
  - `fluxloop generate inputs` – produce input variations for the active project
67
67
  - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
68
68
  - `fluxloop parse experiment` – convert experiment outputs into readable artifacts
69
- - `fluxloop evaluate experiment` – score experiment outputs and generate evaluation reports
69
+ - `fluxloop evaluate experiment` – score experiment outputs using rule-based and LLM evaluators, generate reports with success criteria, analysis, and customizable templates
70
70
  - `fluxloop config set-llm` – update LLM provider/model in `configs/input.yaml`
71
71
  - `fluxloop record enable|disable|status` – toggle recording mode across `.env` and simulation config
72
72
  - `fluxloop doctor` – summarize Python, FluxLoop CLI/MCP, and MCP index state for the active environment
@@ -26,7 +26,7 @@ The legacy `setting.yaml` is still supported, but new projects created with
26
26
  - `fluxloop generate inputs` – produce input variations for the active project
27
27
  - `fluxloop run experiment` – execute an experiment using `configs/simulation.yaml`
28
28
  - `fluxloop parse experiment` – convert experiment outputs into readable artifacts
29
- - `fluxloop evaluate experiment` – score experiment outputs and generate evaluation reports
29
+ - `fluxloop evaluate experiment` – score experiment outputs using rule-based and LLM evaluators, generate reports with success criteria, analysis, and customizable templates
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
@@ -2,7 +2,7 @@
2
2
  FluxLoop CLI - Command-line interface for running agent simulations.
3
3
  """
4
4
 
5
- __version__ = "0.2.18"
5
+ __version__ = "0.2.20"
6
6
 
7
7
  from .main import app
8
8
 
@@ -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():
@@ -32,6 +32,7 @@ class EvaluatorConfig:
32
32
  prompt_template: Optional[str] = None
33
33
  max_score: Optional[float] = None
34
34
  parser: Optional[str] = None
35
+ model_parameters: Dict[str, Any] = field(default_factory=dict)
35
36
  metadata: Dict[str, Any] = field(default_factory=dict)
36
37
 
37
38
 
@@ -281,6 +282,14 @@ def _parse_evaluators(raw_evaluators: Any) -> List[EvaluatorConfig]:
281
282
  if evaluator_type not in {"rule_based", "llm_judge"}:
282
283
  raise ValueError(f"Unsupported evaluator type: {evaluator_type}")
283
284
 
285
+ model_parameters_raw = entry.get("model_parameters")
286
+ if model_parameters_raw is None:
287
+ model_parameters: Dict[str, Any] = {}
288
+ elif isinstance(model_parameters_raw, dict):
289
+ model_parameters = dict(model_parameters_raw)
290
+ else:
291
+ raise ValueError("model_parameters must be a mapping when provided")
292
+
284
293
  config = EvaluatorConfig(
285
294
  name=str(entry["name"]),
286
295
  type=evaluator_type, # type: ignore[arg-type]
@@ -291,6 +300,7 @@ def _parse_evaluators(raw_evaluators: Any) -> List[EvaluatorConfig]:
291
300
  prompt_template=entry.get("prompt_template"),
292
301
  max_score=entry.get("max_score"),
293
302
  parser=entry.get("parser"),
303
+ model_parameters=model_parameters,
294
304
  metadata={
295
305
  k: v
296
306
  for k, v in entry.items()
@@ -305,6 +315,7 @@ def _parse_evaluators(raw_evaluators: Any) -> List[EvaluatorConfig]:
305
315
  "prompt_template",
306
316
  "max_score",
307
317
  "parser",
318
+ "model_parameters",
308
319
  }
309
320
  },
310
321
  )
@@ -188,6 +188,141 @@ def _compare_to_baseline(current: Dict[str, Any], baseline: Dict[str, Any]) -> D
188
188
  return comparisons
189
189
 
190
190
 
191
+ def _format_percent(value: Optional[float]) -> str:
192
+ if value is None:
193
+ return "—"
194
+ return f"{value * 100:.1f}%"
195
+
196
+
197
+ def _generate_recommendations(
198
+ summary: Dict[str, Any],
199
+ analysis: Dict[str, Any],
200
+ ) -> List[Dict[str, Any]]:
201
+ items: List[Dict[str, Any]] = []
202
+
203
+ pass_rate = summary.get("pass_rate")
204
+ threshold = summary.get("threshold")
205
+ if isinstance(pass_rate, (int, float)) and isinstance(threshold, (int, float)):
206
+ if pass_rate < threshold:
207
+ top_reason = None
208
+ top_reasons = summary.get("top_reasons") or []
209
+ if top_reasons:
210
+ top_reason = top_reasons[0][0]
211
+ summary_text = (
212
+ f"Pass rate is {_format_percent(pass_rate)} (goal {_format_percent(threshold)})."
213
+ )
214
+ if top_reason:
215
+ summary_text += f" Most common failure reason: {top_reason}."
216
+ items.append(
217
+ {
218
+ "title": "Boost overall pass rate",
219
+ "priority": "high",
220
+ "summary": summary_text,
221
+ "metrics": {
222
+ "pass_rate": pass_rate,
223
+ "threshold": threshold,
224
+ },
225
+ }
226
+ )
227
+
228
+ persona_breakdown = summary.get("persona_breakdown") or {}
229
+ if isinstance(persona_breakdown, dict) and isinstance(threshold, (int, float)):
230
+ for persona, stats in persona_breakdown.items():
231
+ persona_pass_rate = stats.get("pass_rate")
232
+ if isinstance(persona_pass_rate, (int, float)) and persona_pass_rate < threshold:
233
+ items.append(
234
+ {
235
+ "title": f"Target persona '{persona}'",
236
+ "priority": "medium",
237
+ "summary": (
238
+ f"{persona} pass rate is {_format_percent(persona_pass_rate)} "
239
+ f"(goal {_format_percent(threshold)}). Prioritize playbooks or prompts for this persona."
240
+ ),
241
+ "metrics": {
242
+ "persona": persona,
243
+ "pass_rate": persona_pass_rate,
244
+ "threshold": threshold,
245
+ },
246
+ }
247
+ )
248
+
249
+ comparison = analysis.get("comparison")
250
+ if isinstance(comparison, dict):
251
+ evaluator_deltas = comparison.get("evaluator_averages") or {}
252
+ if isinstance(evaluator_deltas, dict):
253
+ for evaluator_name, payload in evaluator_deltas.items():
254
+ delta = payload.get("delta")
255
+ if isinstance(delta, (int, float)) and delta < -0.05:
256
+ items.append(
257
+ {
258
+ "title": f"Recover regression in {evaluator_name}",
259
+ "priority": "high",
260
+ "summary": (
261
+ f"{evaluator_name} average score dropped by {delta:.3f} "
262
+ "vs. baseline. Investigate prompt or flow changes."
263
+ ),
264
+ "metrics": {
265
+ "evaluator": evaluator_name,
266
+ "delta": delta,
267
+ "baseline": payload.get("baseline"),
268
+ "current": payload.get("current"),
269
+ },
270
+ }
271
+ )
272
+
273
+ failures = analysis.get("failures")
274
+ if isinstance(failures, dict):
275
+ categorized = failures.get("categorized") or {}
276
+ if isinstance(categorized, dict) and categorized:
277
+ top_failure = max(categorized.items(), key=lambda item: item[1])
278
+ category, count = top_failure
279
+ if count:
280
+ items.append(
281
+ {
282
+ "title": f"Address {category.replace('_', ' ')} failures",
283
+ "priority": "medium",
284
+ "summary": (
285
+ f"{count} traces failed due to {category.replace('_', ' ')} issues. "
286
+ "Review tooling and guardrails for this category."
287
+ ),
288
+ "metrics": {
289
+ "category": category,
290
+ "count": count,
291
+ },
292
+ }
293
+ )
294
+
295
+ performance = analysis.get("performance")
296
+ if isinstance(performance, dict):
297
+ trends = performance.get("trends") or {}
298
+ if isinstance(trends, dict):
299
+ final_score_trend = trends.get("final_score")
300
+ if isinstance(final_score_trend, dict) and final_score_trend.get("trend") == "decreasing":
301
+ slope = final_score_trend.get("slope")
302
+ items.append(
303
+ {
304
+ "title": "Investigate declining trend",
305
+ "priority": "medium",
306
+ "summary": (
307
+ "Average final score shows a decreasing trend. "
308
+ "Review recent dataset or agent updates."
309
+ )
310
+ + (f" Slope: {slope:.3f}." if isinstance(slope, (int, float)) else ""),
311
+ "metrics": {
312
+ "trend": final_score_trend.get("trend"),
313
+ "slope": slope,
314
+ },
315
+ }
316
+ )
317
+
318
+ # Deduplicate by title to avoid noisy recommendations.
319
+ unique: Dict[str, Dict[str, Any]] = {}
320
+ for item in items:
321
+ unique.setdefault(item["title"], item)
322
+
323
+ return list(unique.values())
324
+
325
+
191
326
  def compute_additional_analysis(
192
327
  results: List["TraceOutcome"],
193
328
  summary: Dict[str, Any],
@@ -255,6 +390,10 @@ def compute_additional_analysis(
255
390
  analysis.setdefault("comparison", {})
256
391
  analysis["comparison"]["message"] = "Comparison enabled but no baseline path provided."
257
392
 
393
+ recommendations = _generate_recommendations(summary, analysis)
394
+ if recommendations:
395
+ analysis["recommendations"] = recommendations
396
+
258
397
  return analysis
259
398
 
260
399