fluxloop-cli 0.2.26__tar.gz → 0.2.27__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 (65) hide show
  1. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/PKG-INFO +1 -1
  2. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/__init__.py +1 -1
  3. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/html.py +86 -5
  4. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/rules.py +167 -0
  5. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/templates.py +4 -4
  6. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/PKG-INFO +1 -1
  7. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/pyproject.toml +1 -1
  8. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_evaluate_command.py +38 -6
  9. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/README.md +0 -0
  10. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/arg_binder.py +0 -0
  11. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/__init__.py +0 -0
  12. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/config.py +0 -0
  13. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/doctor.py +0 -0
  14. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/evaluate.py +0 -0
  15. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/generate.py +0 -0
  16. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/init.py +0 -0
  17. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/parse.py +0 -0
  18. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/record.py +0 -0
  19. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/run.py +0 -0
  20. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/status.py +0 -0
  21. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/config_loader.py +0 -0
  22. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/config_schema.py +0 -0
  23. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/constants.py +0 -0
  24. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/conversation_supervisor.py +0 -0
  25. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/environment.py +0 -0
  26. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/__init__.py +0 -0
  27. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/artifacts.py +0 -0
  28. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/config.py +0 -0
  29. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
  30. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
  31. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/core.py +0 -0
  32. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
  33. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
  34. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/success.py +0 -0
  35. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/llm.py +0 -0
  36. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
  37. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/base.py +0 -0
  38. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/information_completeness.py +0 -0
  39. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/intent_recognition.py +0 -0
  40. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/response_clarity.py +0 -0
  41. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/response_consistency.py +0 -0
  42. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/input_generator.py +0 -0
  43. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/llm_generator.py +0 -0
  44. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/main.py +0 -0
  45. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/project_paths.py +0 -0
  46. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/runner.py +0 -0
  47. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/target_loader.py +0 -0
  48. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/validators.py +0 -0
  49. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/SOURCES.txt +0 -0
  50. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
  51. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/entry_points.txt +0 -0
  52. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/requires.txt +0 -0
  53. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/top_level.txt +0 -0
  54. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/setup.cfg +0 -0
  55. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_analysis_recommendations.py +0 -0
  56. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_arg_binder.py +0 -0
  57. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_config_command.py +0 -0
  58. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_conversation_supervisor.py +0 -0
  59. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_evaluation_llm.py +0 -0
  60. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_input_generator.py +0 -0
  61. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_prompt_library.py +0 -0
  62. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_run_command.py +0 -0
  63. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_runner_multi_turn.py +0 -0
  64. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_success_criteria.py +0 -0
  65. {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/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.26
3
+ Version: 0.2.27
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.26"
5
+ __version__ = "0.2.27"
6
6
 
7
7
  from .main import app
8
8
 
@@ -40,6 +40,7 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
40
40
  <head>
41
41
  <meta charset="utf-8" />
42
42
  <meta name="viewport" content="width=device-width, initial-scale=1" />
43
+ <meta name="report:generated_at" content="[[DATE]]" />
43
44
  <title>[[TITLE]]</title>
44
45
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css">
45
46
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
@@ -47,9 +48,19 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
47
48
  <body class="bg-slate-950 text-slate-100">
48
49
  <main class="max-w-6xl mx-auto p-8 space-y-6">
49
50
  <header class="space-y-6">
50
- <div>
51
- <h1 class="text-3xl font-bold">[[TITLE]]</h1>
52
- <p class="text-sm text-slate-300">Generated at [[DATE]]</p>
51
+ <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
52
+ <div>
53
+ <h1 class="text-3xl font-bold">[[TITLE]]</h1>
54
+ <p class="text-sm text-slate-300">[[SUBTITLE]]</p>
55
+ </div>
56
+ <a
57
+ href="https://fluxloop.io/"
58
+ class="inline-flex items-center gap-2 self-start rounded-full border border-sky-400 px-4 py-2 text-sm font-semibold text-sky-200 transition hover:bg-sky-500/10 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 focus:ring-offset-slate-950"
59
+ target="_blank"
60
+ rel="noopener noreferrer"
61
+ >
62
+ <span>Built with FluxLoop</span>
63
+ </a>
53
64
  </div>
54
65
  <nav class="flex flex-wrap gap-2" role="tablist" aria-label="Report sections">
55
66
  <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>
@@ -62,6 +73,11 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
62
73
  <section data-tab-panel="summary" class="tab-panel space-y-6">
63
74
  <div id="summaryCards" class="grid gap-4 md:grid-cols-2 xl:grid-cols-4"></div>
64
75
 
76
+ <section class="space-y-4">
77
+ <h2 class="text-xl font-semibold">Evaluation Goal</h2>
78
+ <div id="evaluationGoal" class="whitespace-pre-wrap rounded-xl border border-slate-800 bg-slate-900 p-4 text-sm text-slate-300"></div>
79
+ </section>
80
+
65
81
  <section class="space-y-4">
66
82
  <h2 class="text-xl font-semibold">Success Criteria</h2>
67
83
  <div id="criteriaList" class="space-y-3"></div>
@@ -214,6 +230,21 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
214
230
  .join("");
215
231
  }
216
232
 
233
+ function renderEvaluationGoal() {
234
+ const container = document.getElementById("evaluationGoal");
235
+ if (!container) return;
236
+ const goal = summary?.evaluation_goal;
237
+ if (!goal) {
238
+ container.textContent = "No evaluation goal provided.";
239
+ container.classList.remove("text-slate-300");
240
+ container.classList.add("text-slate-400");
241
+ return;
242
+ }
243
+ container.classList.remove("text-slate-400");
244
+ container.classList.add("text-slate-300");
245
+ container.textContent = goal;
246
+ }
247
+
217
248
  function renderCriteria() {
218
249
  const container = document.getElementById("criteriaList");
219
250
  if (!container || !criteria || !Object.keys(criteria).length) {
@@ -458,6 +489,7 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
458
489
 
459
490
  initTabs();
460
491
  renderSummaryCards();
492
+ renderEvaluationGoal();
461
493
  renderCriteria();
462
494
  renderRecommendations();
463
495
  renderScoreChart();
@@ -471,6 +503,45 @@ DEFAULT_TEMPLATE = r"""<!DOCTYPE html>
471
503
  """
472
504
 
473
505
 
506
+ def _resolve_experiment_label(summary: Dict[str, Any], output_path: Path) -> Optional[str]:
507
+ def _stringify(value: Any) -> Optional[str]:
508
+ if value is None:
509
+ return None
510
+ if isinstance(value, (list, tuple, set)):
511
+ parts = [str(item) for item in value if item not in (None, "")]
512
+ if not parts:
513
+ return None
514
+ return ", ".join(parts)
515
+ if isinstance(value, str):
516
+ return value.strip() or None
517
+ return str(value)
518
+
519
+ for key in ("experiment_id", "experiment_name"):
520
+ candidate = _stringify(summary.get(key))
521
+ if candidate:
522
+ return candidate
523
+
524
+ experiment_meta = summary.get("experiment")
525
+ if isinstance(experiment_meta, dict):
526
+ for key in ("name", "id", "label"):
527
+ candidate = _stringify(experiment_meta.get(key))
528
+ if candidate:
529
+ return candidate
530
+
531
+ output_dir = output_path.parent
532
+ if output_dir != output_path:
533
+ experiment_dir = output_dir.parent
534
+ if experiment_dir and experiment_dir != output_dir:
535
+ candidate = _stringify(experiment_dir.name)
536
+ if candidate:
537
+ return candidate
538
+ candidate = _stringify(output_dir.name)
539
+ if candidate:
540
+ return candidate
541
+
542
+ return None
543
+
544
+
474
545
  def select_html_template(options: "EvaluationOptions", config: "EvaluationConfig") -> Tuple[str, Optional[str]]:
475
546
  if options.report_template:
476
547
  template_text = load_template_from_path(options.report_template)
@@ -497,9 +568,19 @@ def write_html_report(
497
568
  success_criteria = summary.get("success_criteria_results") or {}
498
569
  analysis = summary.get("analysis") or {}
499
570
 
571
+ generated_at = datetime.now(UTC)
572
+ generated_iso = generated_at.isoformat(timespec="seconds").replace("+00:00", "Z")
573
+ human_readable = generated_at.strftime("%Y-%m-%d %H:%M:%SZ")
574
+ experiment_label = _resolve_experiment_label(summary, output_path)
575
+ subtitle_parts = [f"Generated on {human_readable}"]
576
+ if experiment_label:
577
+ subtitle_parts.append(f"Experiment: {experiment_label}")
578
+ subtitle = " · ".join(subtitle_parts)
579
+
500
580
  replacements = {
501
- "[[TITLE]]": summary.get("evaluation_goal") or "FluxLoop Evaluation Report",
502
- "[[DATE]]": datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z"),
581
+ "[[TITLE]]": "FluxLoop Evaluation Report",
582
+ "[[DATE]]": generated_iso,
583
+ "[[SUBTITLE]]": subtitle,
503
584
  "[[SUMMARY_JSON]]": json.dumps(summary, ensure_ascii=False),
504
585
  "[[PER_TRACE_JSON]]": json.dumps(per_trace_payload, ensure_ascii=False),
505
586
  "[[CRITERIA_JSON]]": json.dumps(success_criteria, ensure_ascii=False),
@@ -189,6 +189,172 @@ def _evaluate_latency_under(context: RuleContext, definition: RuleDefinition) ->
189
189
  return RuleResult(rule=definition, score=score, reason=reason)
190
190
 
191
191
 
192
+ def _coerce_usage_values(data: Dict[str, Any]) -> Dict[str, float]:
193
+ usage: Dict[str, float] = {}
194
+ for key in ("prompt_tokens", "completion_tokens", "total_tokens"):
195
+ value = data.get(key)
196
+ if isinstance(value, (int, float)):
197
+ usage[key] = float(value)
198
+ return usage
199
+
200
+
201
+ def _usage_from_candidate(candidate: Any) -> Dict[str, float]:
202
+ if not isinstance(candidate, dict):
203
+ return {}
204
+ direct = _coerce_usage_values(candidate)
205
+ if direct:
206
+ return direct
207
+ token_usage = candidate.get("token_usage")
208
+ if isinstance(token_usage, dict):
209
+ values = _coerce_usage_values(token_usage)
210
+ if values:
211
+ return values
212
+ nested = candidate.get("usage")
213
+ if isinstance(nested, dict):
214
+ return _coerce_usage_values(nested)
215
+ return {}
216
+
217
+
218
+ def _entry_token_usage(entry: Dict[str, Any]) -> Dict[str, float]:
219
+ candidates = [entry]
220
+ output = entry.get("output")
221
+ if isinstance(output, dict):
222
+ candidates.append(output)
223
+ messages = output.get("messages")
224
+ if isinstance(messages, dict):
225
+ candidates.append(messages)
226
+ response_metadata = messages.get("response_metadata")
227
+ if isinstance(response_metadata, dict):
228
+ candidates.append(response_metadata)
229
+ raw = entry.get("raw")
230
+ if isinstance(raw, dict):
231
+ candidates.append(raw)
232
+ raw_output = raw.get("output")
233
+ if isinstance(raw_output, dict):
234
+ candidates.append(raw_output)
235
+ response_metadata = raw_output.get("response_metadata")
236
+ if isinstance(response_metadata, dict):
237
+ candidates.append(response_metadata)
238
+ messages = raw_output.get("messages")
239
+ if isinstance(messages, dict):
240
+ candidates.append(messages)
241
+ response_metadata = messages.get("response_metadata")
242
+ if isinstance(response_metadata, dict):
243
+ candidates.append(response_metadata)
244
+ metadata = raw.get("metadata")
245
+ if isinstance(metadata, dict):
246
+ candidates.append(metadata)
247
+ response_metadata = raw.get("response_metadata")
248
+ if isinstance(response_metadata, dict):
249
+ candidates.append(response_metadata)
250
+ for candidate in candidates:
251
+ usage = _usage_from_candidate(candidate)
252
+ if usage:
253
+ return usage
254
+ return {}
255
+
256
+
257
+ def _collect_token_usage(trace: Dict[str, Any]) -> Optional[Dict[str, float]]:
258
+ totals = {"prompt_tokens": 0.0, "completion_tokens": 0.0, "total_tokens": 0.0}
259
+ found = False
260
+
261
+ summary = trace.get("summary")
262
+ if isinstance(summary, dict):
263
+ summary_usage = {}
264
+ if isinstance(summary.get("token_usage"), dict):
265
+ summary_usage = _usage_from_candidate(summary["token_usage"])
266
+ if not summary_usage:
267
+ raw = summary.get("raw")
268
+ if isinstance(raw, dict) and isinstance(raw.get("token_usage"), dict):
269
+ summary_usage = _usage_from_candidate(raw["token_usage"])
270
+ if summary_usage:
271
+ found = True
272
+ for key, value in summary_usage.items():
273
+ totals[key] += value
274
+
275
+ timeline = trace.get("timeline")
276
+ if isinstance(timeline, list):
277
+ for entry in timeline:
278
+ if not isinstance(entry, dict):
279
+ continue
280
+ usage = _entry_token_usage(entry)
281
+ if not usage:
282
+ continue
283
+ found = True
284
+ for key, value in usage.items():
285
+ totals[key] += value
286
+
287
+ if not found:
288
+ return None
289
+
290
+ prompt = totals["prompt_tokens"]
291
+ completion = totals["completion_tokens"]
292
+ total = totals["total_tokens"]
293
+ if total <= 0:
294
+ total = prompt + completion
295
+ return {
296
+ "prompt": prompt,
297
+ "completion": completion,
298
+ "total": total,
299
+ }
300
+
301
+
302
+ def _coerce_budget_value(value: Any, label: str) -> Optional[float]:
303
+ if value is None:
304
+ return None
305
+ try:
306
+ budget = float(value)
307
+ except (TypeError, ValueError) as exc: # noqa: BLE001
308
+ raise ValueError(f"token_usage_under {label} must be numeric") from exc
309
+ if budget <= 0:
310
+ raise ValueError(f"token_usage_under {label} must be greater than 0")
311
+ return budget
312
+
313
+
314
+ def _evaluate_token_usage_under(context: RuleContext, definition: RuleDefinition) -> RuleResult:
315
+ usage = _collect_token_usage(context.trace)
316
+ if usage is None:
317
+ return RuleResult(
318
+ rule=definition,
319
+ score=0.0,
320
+ reason="token usage data not available",
321
+ )
322
+
323
+ max_total = _coerce_budget_value(definition.params.get("max_total_tokens"), "max_total_tokens")
324
+ max_prompt = _coerce_budget_value(
325
+ definition.params.get("max_prompt_tokens"),
326
+ "max_prompt_tokens",
327
+ )
328
+ max_completion = _coerce_budget_value(
329
+ definition.params.get("max_completion_tokens"),
330
+ "max_completion_tokens",
331
+ )
332
+
333
+ if max_total is None and max_prompt is None and max_completion is None:
334
+ raise ValueError("token_usage_under requires at least one max_*_tokens parameter")
335
+
336
+ score = 1.0
337
+ reasons: List[str] = []
338
+
339
+ def apply_budget(actual: float, budget: Optional[float], label: str) -> None:
340
+ nonlocal score
341
+ if budget is None:
342
+ return
343
+ if actual <= budget:
344
+ return
345
+ ratio = budget / actual if actual > 0 else 0.0
346
+ ratio = max(0.0, min(1.0, ratio))
347
+ score = min(score, ratio)
348
+ reasons.append(f"{label} {actual:.0f} exceeds budget {budget:.0f}")
349
+
350
+ apply_budget(usage["total"], max_total, "total tokens")
351
+ apply_budget(usage["prompt"], max_prompt, "prompt tokens")
352
+ apply_budget(usage["completion"], max_completion, "completion tokens")
353
+
354
+ reason = "; ".join(reasons) if reasons else None
355
+ return RuleResult(rule=definition, score=score, reason=reason)
356
+
357
+
192
358
  def _evaluate_similarity(context: RuleContext, definition: RuleDefinition) -> RuleResult:
193
359
  target = definition.params.get("target", "output")
194
360
  expected_path = definition.params.get("expected_path")
@@ -233,6 +399,7 @@ RULE_DISPATCH = {
233
399
  "not_contains": _evaluate_not_contains,
234
400
  "matches_regex": _evaluate_matches_regex,
235
401
  "latency_under": _evaluate_latency_under,
402
+ "token_usage_under": _evaluate_token_usage_under,
236
403
  "similarity": _evaluate_similarity,
237
404
  "success": _evaluate_success_flag,
238
405
  }
@@ -216,13 +216,13 @@ def create_evaluation_config() -> str:
216
216
  rules:
217
217
  - check: output_not_empty
218
218
 
219
- - name: latency_budget
219
+ - name: token_budget
220
220
  type: rule_based
221
221
  enabled: true
222
222
  weight: 0.2
223
223
  rules:
224
- - check: latency_under
225
- budget_ms: 1500
224
+ - check: token_usage_under
225
+ max_total_tokens: 4000
226
226
 
227
227
  - name: keyword_quality
228
228
  type: rule_based
@@ -340,7 +340,7 @@ def create_evaluation_config() -> str:
340
340
  performance:
341
341
  all_traces_successful: true
342
342
  avg_response_time:
343
- enabled: true
343
+ enabled: false
344
344
  threshold_ms: 2000
345
345
  max_response_time:
346
346
  enabled: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluxloop-cli
3
- Version: 0.2.26
3
+ Version: 0.2.27
4
4
  Summary: FluxLoop CLI for running agent simulations
5
5
  Author-email: FluxLoop Team <team@fluxloop.dev>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fluxloop-cli"
7
- version = "0.2.26"
7
+ version = "0.2.27"
8
8
  description = "FluxLoop CLI for running agent simulations"
9
9
  authors = [
10
10
  {name = "FluxLoop Team", email = "team@fluxloop.dev"}
@@ -19,6 +19,11 @@ def _write_trace_summary(path: Path) -> None:
19
19
  "output": "Sure, I can help you.",
20
20
  "duration_ms": 500,
21
21
  "success": True,
22
+ "token_usage": {
23
+ "prompt_tokens": 600,
24
+ "completion_tokens": 200,
25
+ "total_tokens": 800,
26
+ },
22
27
  },
23
28
  {
24
29
  "trace_id": "trace-2",
@@ -28,6 +33,11 @@ def _write_trace_summary(path: Path) -> None:
28
33
  "output": "I cannot assist right now.",
29
34
  "duration_ms": 1500,
30
35
  "success": False,
36
+ "token_usage": {
37
+ "prompt_tokens": 900,
38
+ "completion_tokens": 500,
39
+ "total_tokens": 1400,
40
+ },
31
41
  },
32
42
  ]
33
43
  lines = [json.dumps(item) for item in traces]
@@ -43,8 +53,8 @@ def _write_eval_config(path: Path, include_llm: bool = False) -> None:
43
53
  weight: 1.0
44
54
  rules:
45
55
  - check: output_not_empty
46
- - check: latency_under
47
- budget_ms: 1200
56
+ - check: token_usage_under
57
+ max_total_tokens: 1200
48
58
  - check: success
49
59
 
50
60
  aggregate:
@@ -61,6 +71,8 @@ def _write_eval_config(path: Path, include_llm: bool = False) -> None:
61
71
  weight: 1.0
62
72
  rules:
63
73
  - check: output_not_empty
74
+ - check: token_usage_under
75
+ max_total_tokens: 1200
64
76
  - name: llm_quality
65
77
  type: llm_judge
66
78
  enabled: true
@@ -91,6 +103,11 @@ def _write_phase2_trace_summary(path: Path) -> None:
91
103
  "output": "Alerts configured successfully.",
92
104
  "duration_ms": 900,
93
105
  "success": True,
106
+ "token_usage": {
107
+ "prompt_tokens": 500,
108
+ "completion_tokens": 200,
109
+ "total_tokens": 700,
110
+ },
94
111
  },
95
112
  {
96
113
  "trace_id": "p2-2",
@@ -100,6 +117,11 @@ def _write_phase2_trace_summary(path: Path) -> None:
100
117
  "output": "Retrying the tool call now.",
101
118
  "duration_ms": 1800,
102
119
  "success": True,
120
+ "token_usage": {
121
+ "prompt_tokens": 650,
122
+ "completion_tokens": 250,
123
+ "total_tokens": 900,
124
+ },
103
125
  },
104
126
  {
105
127
  "trace_id": "p2-3",
@@ -109,6 +131,11 @@ def _write_phase2_trace_summary(path: Path) -> None:
109
131
  "output": "",
110
132
  "duration_ms": 650,
111
133
  "success": False,
134
+ "token_usage": {
135
+ "prompt_tokens": 450,
136
+ "completion_tokens": 300,
137
+ "total_tokens": 750,
138
+ },
112
139
  },
113
140
  {
114
141
  "trace_id": "p2-4",
@@ -118,6 +145,11 @@ def _write_phase2_trace_summary(path: Path) -> None:
118
145
  "output": "Incident report created.",
119
146
  "duration_ms": 2100,
120
147
  "success": True,
148
+ "token_usage": {
149
+ "prompt_tokens": 800,
150
+ "completion_tokens": 350,
151
+ "total_tokens": 1150,
152
+ },
121
153
  },
122
154
  ]
123
155
  lines = [json.dumps(item) for item in traces]
@@ -131,13 +163,13 @@ def _write_phase2_eval_config(path: Path) -> None:
131
163
  Validate extended Phase 2 evaluation outputs for persona-aware runs.
132
164
 
133
165
  evaluators:
134
- - name: latency_checker
166
+ - name: token_checker
135
167
  type: rule_based
136
168
  enabled: true
137
169
  weight: 1.0
138
170
  rules:
139
- - check: latency_under
140
- budget_ms: 1500
171
+ - check: token_usage_under
172
+ max_total_tokens: 1500
141
173
  - name: intent_recognition
142
174
  type: rule_based
143
175
  enabled: true
@@ -201,7 +233,7 @@ def _write_baseline_summary(path: Path) -> None:
201
233
  "total_traces": 4,
202
234
  "passed_traces": 3,
203
235
  "evaluator_stats": {
204
- "latency_checker": {"average": 0.75, "min": 0.0, "max": 1.0, "count": 4},
236
+ "token_checker": {"average": 0.75, "min": 0.0, "max": 1.0, "count": 4},
205
237
  "intent_recognition": {"average": 1.0, "min": 1.0, "max": 1.0, "count": 4},
206
238
  },
207
239
  }
File without changes
File without changes