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.
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/PKG-INFO +2 -2
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/README.md +1 -1
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/__init__.py +1 -1
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/init.py +2 -18
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/config.py +11 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/analysis.py +139 -0
- fluxloop_cli-0.2.20/fluxloop_cli/evaluation/engine/reporting/html.py +523 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/reporting/markdown.py +10 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/llm.py +55 -2
- fluxloop_cli-0.2.20/fluxloop_cli/evaluation/prompts/__init__.py +182 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/templates.py +210 -209
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/PKG-INFO +2 -2
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/SOURCES.txt +5 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/pyproject.toml +1 -1
- fluxloop_cli-0.2.20/tests/test_analysis_recommendations.py +82 -0
- fluxloop_cli-0.2.20/tests/test_evaluation_llm.py +92 -0
- fluxloop_cli-0.2.20/tests/test_prompt_library.py +29 -0
- fluxloop_cli-0.2.20/tests/test_success_criteria.py +54 -0
- fluxloop_cli-0.2.18/fluxloop_cli/evaluation/engine/reporting/html.py +0 -257
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/arg_binder.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/__init__.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/config.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/doctor.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/evaluate.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/generate.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/parse.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/record.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/run.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/commands/status.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/config_loader.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/config_schema.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/constants.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/__init__.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/__init__.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/core.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/reporting/__init__.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/engine/success.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/evaluation/rules.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/input_generator.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/llm_generator.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/main.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/project_paths.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/runner.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/target_loader.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli/validators.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/dependency_links.txt +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/entry_points.txt +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/requires.txt +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/fluxloop_cli.egg-info/top_level.txt +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/setup.cfg +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_arg_binder.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_config_command.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_evaluate_command.py +0 -0
- {fluxloop_cli-0.2.18 → fluxloop_cli-0.2.20}/tests/test_input_generator.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
@@ -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 (
|
|
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
|
|