applied-cli 0.6.1__tar.gz → 0.6.2__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 (66) hide show
  1. {applied_cli-0.6.1 → applied_cli-0.6.2}/PKG-INFO +1 -1
  2. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/cli.py +32 -0
  3. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/tools.py +88 -4
  4. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/scenarios.py +31 -6
  5. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli.egg-info/PKG-INFO +1 -1
  6. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli.egg-info/SOURCES.txt +1 -0
  7. {applied_cli-0.6.1 → applied_cli-0.6.2}/pyproject.toml +1 -1
  8. applied_cli-0.6.2/tests/test_benchmark_delete_guardrail.py +83 -0
  9. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_scenarios.py +34 -12
  10. {applied_cli-0.6.1 → applied_cli-0.6.2}/README.md +0 -0
  11. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/__init__.py +0 -0
  12. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/agent_scoped_flows.py +0 -0
  13. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/auth.py +0 -0
  14. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/client.py +0 -0
  15. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/conversation_lookup.py +0 -0
  16. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/conversations.py +0 -0
  17. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/credentials.py +0 -0
  18. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/flow_helpers.py +0 -0
  19. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/formatters.py +0 -0
  20. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/mcp.py +0 -0
  21. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/recovery.py +0 -0
  22. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/toolkit.py +0 -0
  23. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/__init__.py +0 -0
  24. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/agents.py +0 -0
  25. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/articles.py +0 -0
  26. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/catalog.py +0 -0
  27. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/connectors.py +0 -0
  28. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/content.py +0 -0
  29. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/conversations.py +0 -0
  30. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/domains.py +0 -0
  31. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/flows.py +0 -0
  32. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/knowledge.py +0 -0
  33. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/manifest.py +0 -0
  34. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/products.py +0 -0
  35. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/taxonomy.py +0 -0
  36. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli/v2/tickets.py +0 -0
  37. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli.egg-info/dependency_links.txt +0 -0
  38. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli.egg-info/entry_points.txt +0 -0
  39. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli.egg-info/requires.txt +0 -0
  40. {applied_cli-0.6.1 → applied_cli-0.6.2}/applied_cli.egg-info/top_level.txt +0 -0
  41. {applied_cli-0.6.1 → applied_cli-0.6.2}/setup.cfg +0 -0
  42. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_agent_scoped_flows.py +0 -0
  43. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_audit_tools.py +0 -0
  44. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_auth_context.py +0 -0
  45. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_benchmark_clone.py +0 -0
  46. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_benchmark_scenario_tools.py +0 -0
  47. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_cli.py +0 -0
  48. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_cli_v2.py +0 -0
  49. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_client.py +0 -0
  50. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_client_v2.py +0 -0
  51. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_conversation_tools.py +0 -0
  52. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_flow_tools.py +0 -0
  53. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_knowledge_content_tools.py +0 -0
  54. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_recovery.py +0 -0
  55. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_toolkit_contract.py +0 -0
  56. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_agents.py +0 -0
  57. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_articles.py +0 -0
  58. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_catalog_and_mcp.py +0 -0
  59. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_connectors.py +0 -0
  60. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_content.py +0 -0
  61. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_conversations.py +0 -0
  62. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_flows.py +0 -0
  63. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_knowledge.py +0 -0
  64. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_products.py +0 -0
  65. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_taxonomy.py +0 -0
  66. {applied_cli-0.6.1 → applied_cli-0.6.2}/tests/test_v2_tickets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -1640,6 +1640,38 @@ def benchmark_clone(
1640
1640
  typer.echo(result)
1641
1641
 
1642
1642
 
1643
+ @app.command("benchmark-delete")
1644
+ def benchmark_delete(
1645
+ id: str = typer.Argument(..., help="Benchmark ID"),
1646
+ force: bool = typer.Option(
1647
+ False,
1648
+ "--force",
1649
+ help="Acknowledge and proceed with the cascade delete of scenarios/runs",
1650
+ ),
1651
+ detach_scenarios: bool = typer.Option(
1652
+ False,
1653
+ "--detach-scenarios",
1654
+ help="Preserve scenarios by unlinking them before deleting the benchmark",
1655
+ ),
1656
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1657
+ format: str = typer.Option(
1658
+ "text", "--format", "-f", help="Output format: text or json"
1659
+ ),
1660
+ ) -> None:
1661
+ """Delete a benchmark; refuses to silently wipe scenarios (see --force / --detach-scenarios)."""
1662
+ client = get_client(shop_id=shop_id)
1663
+ result = asyncio.run(
1664
+ tools.benchmark_delete(
1665
+ client,
1666
+ benchmark_id=id,
1667
+ force=force,
1668
+ detach_scenarios=detach_scenarios,
1669
+ output_format=format,
1670
+ )
1671
+ )
1672
+ typer.echo(result)
1673
+
1674
+
1643
1675
  @app.command()
1644
1676
  def scenarios(
1645
1677
  benchmark_id: str = typer.Option(
@@ -5409,23 +5409,107 @@ async def benchmark_create(
5409
5409
  async def benchmark_delete(
5410
5410
  client: AppliedClient,
5411
5411
  benchmark_id: str,
5412
+ *,
5413
+ force: bool = False,
5414
+ detach_scenarios: bool = False,
5415
+ output_format: str = "text",
5412
5416
  ) -> str:
5413
5417
  """
5414
- Delete a benchmark.
5418
+ Delete a benchmark, with a guardrail against silently wiping scenarios.
5419
+
5420
+ On the platform, deleting a benchmark cascades and permanently deletes every
5421
+ scenario and scenario run beneath it. To prevent accidental data loss this
5422
+ refuses to delete a benchmark that still has scenarios unless you either:
5423
+
5424
+ - pass detach_scenarios=True to first unlink the scenarios (they survive
5425
+ under their agent, untagged) and then delete the now-empty benchmark, or
5426
+ - pass force=True to acknowledge and proceed with the cascade delete.
5427
+
5428
+ An empty benchmark is deleted directly.
5415
5429
 
5416
5430
  Args:
5417
5431
  client: Authenticated AppliedClient
5418
5432
  benchmark_id: The benchmark UUID
5433
+ force: Acknowledge and proceed with the cascade delete of scenarios/runs
5434
+ detach_scenarios: Preserve scenarios by unlinking them before deleting
5435
+ output_format: 'text' (default) or 'json'
5419
5436
 
5420
5437
  Returns:
5421
- Success message
5438
+ Result message (or refusal with the impact that would occur).
5422
5439
  """
5423
5440
  try:
5424
- await client.delete_benchmark(benchmark_id)
5441
+ scenarios = await client.list_scenarios(
5442
+ benchmark_id=benchmark_id, fetch_all=True
5443
+ )
5425
5444
  except AppliedAPIError as e:
5426
5445
  return _format_error(e)
5427
5446
 
5428
- return f"Benchmark {benchmark_id} deleted successfully."
5447
+ scenario_count = len(scenarios)
5448
+ run_count = sum(int(s.get("run_count") or 0) for s in scenarios)
5449
+ result: dict[str, Any] = {
5450
+ "benchmark_id": benchmark_id,
5451
+ "scenario_count": scenario_count,
5452
+ "run_count": run_count,
5453
+ "deleted": False,
5454
+ "refused": False,
5455
+ "detached_scenarios": 0,
5456
+ }
5457
+
5458
+ # Preserve scenarios: unlink each from this benchmark, then delete it empty.
5459
+ if scenario_count and detach_scenarios:
5460
+ detached = 0
5461
+ try:
5462
+ for scenario in scenarios:
5463
+ detail = await client.get_scenario(str(scenario["id"]))
5464
+ remaining = [
5465
+ str(b["id"])
5466
+ for b in (detail.get("benchmarks") or [])
5467
+ if b.get("id") and str(b["id"]) != benchmark_id
5468
+ ]
5469
+ await client.update_scenario(
5470
+ str(scenario["id"]), benchmark_ids=remaining
5471
+ )
5472
+ detached += 1
5473
+ await client.delete_benchmark(benchmark_id)
5474
+ except AppliedAPIError as e:
5475
+ result["detached_scenarios"] = detached
5476
+ result["message"] = _format_error(e)
5477
+ return to_json(result) if output_format == "json" else result["message"]
5478
+ result["deleted"] = True
5479
+ result["detached_scenarios"] = detached
5480
+ result["message"] = (
5481
+ f"Detached {detached} scenario(s) and deleted empty benchmark "
5482
+ f"{benchmark_id}. Scenarios preserved under their agent."
5483
+ )
5484
+ return to_json(result) if output_format == "json" else result["message"]
5485
+
5486
+ # Refuse to cascade-delete a non-empty benchmark unless explicitly forced.
5487
+ if scenario_count and not force:
5488
+ result["refused"] = True
5489
+ result["message"] = (
5490
+ f"Refusing to delete benchmark {benchmark_id}: it has "
5491
+ f"{scenario_count} scenario(s) and {run_count} run(s) that would be "
5492
+ f"PERMANENTLY deleted by the cascade. Re-run with detach_scenarios=True "
5493
+ f"to preserve the scenarios, or force=True to delete them anyway."
5494
+ )
5495
+ return to_json(result) if output_format == "json" else result["message"]
5496
+
5497
+ try:
5498
+ await client.delete_benchmark(benchmark_id)
5499
+ except AppliedAPIError as e:
5500
+ result["message"] = _format_error(e)
5501
+ return to_json(result) if output_format == "json" else result["message"]
5502
+
5503
+ result["deleted"] = True
5504
+ result["message"] = (
5505
+ f"Benchmark {benchmark_id} deleted successfully"
5506
+ + (
5507
+ f" (cascaded {scenario_count} scenario(s), {run_count} run(s))."
5508
+ if scenario_count
5509
+ else "."
5510
+ )
5511
+ )
5512
+ return to_json(result) if output_format == "json" else result["message"]
5429
5513
 
5430
5514
 
5431
5515
  def _scenario_input_conversation_id(scenario: dict) -> str | None:
@@ -48,6 +48,8 @@ class BenchmarksCreateInput(StrictInput):
48
48
 
49
49
  class BenchmarksDeleteInput(StrictInput):
50
50
  benchmark_id: str
51
+ force: bool = False
52
+ detach_scenarios: bool = False
51
53
 
52
54
 
53
55
  class BenchmarksCloneInput(StrictInput):
@@ -494,14 +496,33 @@ async def benchmarks_delete_handler(
494
496
  client: AppliedClient,
495
497
  params: BenchmarksDeleteInput,
496
498
  ) -> ToolResult[Any]:
499
+ from applied_cli import tools as legacy_tools
500
+
501
+ raw = await legacy_tools.benchmark_delete(
502
+ client,
503
+ benchmark_id=params.benchmark_id,
504
+ force=params.force,
505
+ detach_scenarios=params.detach_scenarios,
506
+ output_format="json",
507
+ )
497
508
  try:
498
- await client.delete_benchmark(params.benchmark_id)
499
- except AppliedAPIError as exc:
500
- return _api_error_result(exc)
509
+ data = json.loads(raw)
510
+ except (json.JSONDecodeError, TypeError):
511
+ return ToolResult(data={"message": raw}, summary=str(raw))
512
+
513
+ if data.get("refused"):
514
+ return ToolResult(
515
+ data=data,
516
+ summary=data.get("message", "Refused to delete a non-empty benchmark."),
517
+ next_actions=[
518
+ "Re-run with detach_scenarios=true to preserve the scenarios.",
519
+ "Or re-run with force=true to delete the scenarios and runs too.",
520
+ ],
521
+ )
501
522
 
502
523
  return ToolResult(
503
- data={"benchmark_id": params.benchmark_id, "deleted": True},
504
- summary=f"Deleted benchmark {params.benchmark_id}.",
524
+ data=data,
525
+ summary=data.get("message", f"Deleted benchmark {params.benchmark_id}."),
505
526
  next_actions=[
506
527
  "Use benchmarks_list to confirm the benchmark was removed.",
507
528
  ],
@@ -866,7 +887,11 @@ def scenario_specs() -> list[ToolSpec]:
866
887
  ToolSpec(
867
888
  name="benchmarks_delete",
868
889
  namespace="benchmarks",
869
- description="Delete a benchmark and return a structured acknowledgement.",
890
+ description=(
891
+ "Delete a benchmark. Refuses to delete a benchmark that still "
892
+ "has scenarios (the platform cascade would permanently delete "
893
+ "them) unless detach_scenarios=true (preserve them) or force=true."
894
+ ),
870
895
  input_model=BenchmarksDeleteInput,
871
896
  output_model=None,
872
897
  handler=benchmarks_delete_handler,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -39,6 +39,7 @@ tests/test_agent_scoped_flows.py
39
39
  tests/test_audit_tools.py
40
40
  tests/test_auth_context.py
41
41
  tests/test_benchmark_clone.py
42
+ tests/test_benchmark_delete_guardrail.py
42
43
  tests/test_benchmark_scenario_tools.py
43
44
  tests/test_cli.py
44
45
  tests/test_cli_v2.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "applied-cli"
3
- version = "0.6.1"
3
+ version = "0.6.2"
4
4
  description = "CLI and shared client library for Applied Labs AI support agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,83 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from applied_cli import tools
6
+
7
+
8
+ class FakeDeleteClient:
9
+ def __init__(self, scenarios):
10
+ self._scenarios = scenarios
11
+ self.deleted = None
12
+ self.updates = []
13
+
14
+ async def list_scenarios(self, benchmark_id=None, fetch_all=True, **kwargs):
15
+ return list(self._scenarios)
16
+
17
+ async def get_scenario(self, scenario_id):
18
+ for s in self._scenarios:
19
+ if s["id"] == scenario_id:
20
+ return s
21
+ raise KeyError(scenario_id)
22
+
23
+ async def update_scenario(self, scenario_id, **updates):
24
+ self.updates.append({"id": scenario_id, **updates})
25
+ return {"id": scenario_id, **updates}
26
+
27
+ async def delete_benchmark(self, benchmark_id):
28
+ self.deleted = benchmark_id
29
+
30
+
31
+ SCENARIOS = [
32
+ {"id": "s1", "run_count": 3, "benchmarks": [{"id": "bench-1"}]},
33
+ {"id": "s2", "run_count": 1, "benchmarks": [{"id": "bench-1"}, {"id": "bench-2"}]},
34
+ ]
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_empty_benchmark_deletes_directly():
39
+ client = FakeDeleteClient(scenarios=[])
40
+ result = await tools.benchmark_delete(client, "bench-1", output_format="json")
41
+ data = json.loads(result)
42
+ assert data["deleted"] is True
43
+ assert data["refused"] is False
44
+ assert client.deleted == "bench-1"
45
+
46
+
47
+ @pytest.mark.asyncio
48
+ async def test_nonempty_benchmark_refuses_and_discloses_impact():
49
+ client = FakeDeleteClient(scenarios=SCENARIOS)
50
+ result = await tools.benchmark_delete(client, "bench-1") # text, no force
51
+ assert "Refusing to delete" in result
52
+ assert "2 scenario(s)" in result
53
+ assert "4 run(s)" in result # 3 + 1
54
+ assert client.deleted is None # nothing wiped
55
+
56
+
57
+ @pytest.mark.asyncio
58
+ async def test_force_cascades_and_reports_counts():
59
+ client = FakeDeleteClient(scenarios=SCENARIOS)
60
+ result = await tools.benchmark_delete(
61
+ client, "bench-1", force=True, output_format="json"
62
+ )
63
+ data = json.loads(result)
64
+ assert data["deleted"] is True
65
+ assert data["scenario_count"] == 2
66
+ assert data["run_count"] == 4
67
+ assert client.deleted == "bench-1"
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_detach_unlinks_then_deletes_empty_benchmark():
72
+ client = FakeDeleteClient(scenarios=SCENARIOS)
73
+ result = await tools.benchmark_delete(
74
+ client, "bench-1", detach_scenarios=True, output_format="json"
75
+ )
76
+ data = json.loads(result)
77
+ assert data["deleted"] is True
78
+ assert data["detached_scenarios"] == 2
79
+ # bench-1 removed from each scenario's links; bench-2 kept on s2.
80
+ by_id = {u["id"]: u["benchmark_ids"] for u in client.updates}
81
+ assert by_id["s1"] == []
82
+ assert by_id["s2"] == ["bench-2"]
83
+ assert client.deleted == "bench-1"
@@ -429,24 +429,46 @@ async def test_benchmarks_get_includes_structured_scenarios_without_legacy_wrapp
429
429
 
430
430
 
431
431
  @pytest.mark.asyncio
432
- async def test_benchmarks_delete_returns_structured_ack_without_legacy_wrapper(
433
- monkeypatch,
434
- ):
435
- async def fail_legacy_wrapper(*args, **kwargs):
436
- raise AssertionError("legacy benchmark_delete wrapper should not run")
437
-
438
- monkeypatch.setattr("applied_cli.tools.benchmark_delete", fail_legacy_wrapper)
432
+ async def test_benchmarks_delete_refuses_to_wipe_scenarios_without_force():
439
433
  client = FakeScenarioClient()
440
434
  spec = get_tool_catalog()["benchmarks_delete"]
441
435
 
442
436
  result = await spec.run(client, {"benchmark_id": "bench-1"})
443
437
 
444
438
  assert result.ok is True
445
- assert result.summary == "Deleted benchmark bench-1."
446
- assert result.data == {"benchmark_id": "bench-1", "deleted": True}
447
- assert result.next_actions == [
448
- "Use benchmarks_list to confirm the benchmark was removed.",
449
- ]
439
+ assert result.data["refused"] is True
440
+ assert result.data["deleted"] is False
441
+ assert result.data["scenario_count"] >= 1
442
+ assert "Refusing to delete" in result.summary
443
+ # Guardrail must not have deleted the benchmark.
444
+ assert client.delete_benchmark_id is None
445
+
446
+
447
+ @pytest.mark.asyncio
448
+ async def test_benchmarks_delete_force_cascades():
449
+ client = FakeScenarioClient()
450
+ spec = get_tool_catalog()["benchmarks_delete"]
451
+
452
+ result = await spec.run(client, {"benchmark_id": "bench-1", "force": True})
453
+
454
+ assert result.data["deleted"] is True
455
+ assert client.delete_benchmark_id == "bench-1"
456
+
457
+
458
+ @pytest.mark.asyncio
459
+ async def test_benchmarks_delete_detach_preserves_scenarios():
460
+ client = FakeScenarioClient()
461
+ spec = get_tool_catalog()["benchmarks_delete"]
462
+
463
+ result = await spec.run(
464
+ client, {"benchmark_id": "bench-1", "detach_scenarios": True}
465
+ )
466
+
467
+ assert result.data["deleted"] is True
468
+ assert result.data["detached_scenarios"] >= 1
469
+ # Scenarios were unlinked (bench-1 removed from their benchmark list).
470
+ assert client.update_scenario_kwargs is not None
471
+ assert "bench-1" not in client.update_scenario_kwargs["updates"]["benchmark_ids"]
450
472
  assert client.delete_benchmark_id == "bench-1"
451
473
 
452
474
 
File without changes
File without changes