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.
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/PKG-INFO +1 -1
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/__init__.py +1 -1
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/html.py +86 -5
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/rules.py +167 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/templates.py +4 -4
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/PKG-INFO +1 -1
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/pyproject.toml +1 -1
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_evaluate_command.py +38 -6
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/README.md +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/arg_binder.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/__init__.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/config.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/doctor.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/evaluate.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/generate.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/init.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/parse.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/record.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/run.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/commands/status.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/config_loader.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/config_schema.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/constants.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/conversation_supervisor.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/environment.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/__init__.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/artifacts.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/config.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/analysis.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/core.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/markdown.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/success.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/llm.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/__init__.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/base.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/information_completeness.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/intent_recognition.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/response_clarity.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/response_consistency.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/input_generator.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/llm_generator.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/main.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/project_paths.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/runner.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/target_loader.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/validators.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/SOURCES.txt +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/entry_points.txt +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/requires.txt +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli.egg-info/top_level.txt +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/setup.cfg +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_analysis_recommendations.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_arg_binder.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_config_command.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_conversation_supervisor.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_evaluation_llm.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_input_generator.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_prompt_library.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_run_command.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_runner_multi_turn.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_success_criteria.py +0 -0
- {fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/tests/test_target_loader.py +0 -0
{fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/html.py
RENAMED
|
@@ -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
|
-
|
|
52
|
-
|
|
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]]":
|
|
502
|
-
"[[DATE]]":
|
|
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:
|
|
219
|
+
- name: token_budget
|
|
220
220
|
type: rule_based
|
|
221
221
|
enabled: true
|
|
222
222
|
weight: 0.2
|
|
223
223
|
rules:
|
|
224
|
-
- check:
|
|
225
|
-
|
|
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:
|
|
343
|
+
enabled: false
|
|
344
344
|
threshold_ms: 2000
|
|
345
345
|
max_response_time:
|
|
346
346
|
enabled: false
|
|
@@ -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:
|
|
47
|
-
|
|
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:
|
|
166
|
+
- name: token_checker
|
|
135
167
|
type: rule_based
|
|
136
168
|
enabled: true
|
|
137
169
|
weight: 1.0
|
|
138
170
|
rules:
|
|
139
|
-
- check:
|
|
140
|
-
|
|
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
|
-
"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/__init__.py
RENAMED
|
File without changes
|
{fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/engine/reporting/markdown.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/intent_recognition.py
RENAMED
|
File without changes
|
{fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/response_clarity.py
RENAMED
|
File without changes
|
{fluxloop_cli-0.2.26 → fluxloop_cli-0.2.27}/fluxloop_cli/evaluation/prompts/response_consistency.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|