applied-cli 0.6.2__tar.gz → 0.6.3__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 (67) hide show
  1. {applied_cli-0.6.2 → applied_cli-0.6.3}/PKG-INFO +1 -1
  2. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/cli.py +21 -0
  3. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/tools.py +89 -0
  4. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/domains.py +1 -0
  5. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/scenarios.py +51 -0
  6. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/PKG-INFO +1 -1
  7. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/SOURCES.txt +1 -0
  8. {applied_cli-0.6.2 → applied_cli-0.6.3}/pyproject.toml +1 -1
  9. applied_cli-0.6.3/tests/test_scenario_bulk_cancel.py +87 -0
  10. {applied_cli-0.6.2 → applied_cli-0.6.3}/README.md +0 -0
  11. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/__init__.py +0 -0
  12. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/agent_scoped_flows.py +0 -0
  13. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/auth.py +0 -0
  14. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/client.py +0 -0
  15. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/conversation_lookup.py +0 -0
  16. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/conversations.py +0 -0
  17. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/credentials.py +0 -0
  18. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/flow_helpers.py +0 -0
  19. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/formatters.py +0 -0
  20. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/mcp.py +0 -0
  21. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/recovery.py +0 -0
  22. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/toolkit.py +0 -0
  23. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/__init__.py +0 -0
  24. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/agents.py +0 -0
  25. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/articles.py +0 -0
  26. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/catalog.py +0 -0
  27. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/connectors.py +0 -0
  28. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/content.py +0 -0
  29. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/conversations.py +0 -0
  30. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/flows.py +0 -0
  31. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/knowledge.py +0 -0
  32. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/manifest.py +0 -0
  33. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/products.py +0 -0
  34. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/taxonomy.py +0 -0
  35. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli/v2/tickets.py +0 -0
  36. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/dependency_links.txt +0 -0
  37. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/entry_points.txt +0 -0
  38. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/requires.txt +0 -0
  39. {applied_cli-0.6.2 → applied_cli-0.6.3}/applied_cli.egg-info/top_level.txt +0 -0
  40. {applied_cli-0.6.2 → applied_cli-0.6.3}/setup.cfg +0 -0
  41. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_agent_scoped_flows.py +0 -0
  42. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_audit_tools.py +0 -0
  43. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_auth_context.py +0 -0
  44. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_benchmark_clone.py +0 -0
  45. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_benchmark_delete_guardrail.py +0 -0
  46. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_benchmark_scenario_tools.py +0 -0
  47. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_cli.py +0 -0
  48. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_cli_v2.py +0 -0
  49. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_client.py +0 -0
  50. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_client_v2.py +0 -0
  51. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_conversation_tools.py +0 -0
  52. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_flow_tools.py +0 -0
  53. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_knowledge_content_tools.py +0 -0
  54. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_recovery.py +0 -0
  55. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_toolkit_contract.py +0 -0
  56. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_agents.py +0 -0
  57. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_articles.py +0 -0
  58. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_catalog_and_mcp.py +0 -0
  59. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_connectors.py +0 -0
  60. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_content.py +0 -0
  61. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_conversations.py +0 -0
  62. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_flows.py +0 -0
  63. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_knowledge.py +0 -0
  64. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_products.py +0 -0
  65. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_scenarios.py +0 -0
  66. {applied_cli-0.6.2 → applied_cli-0.6.3}/tests/test_v2_taxonomy.py +0 -0
  67. {applied_cli-0.6.2 → applied_cli-0.6.3}/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.2
3
+ Version: 0.6.3
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -1956,6 +1956,27 @@ def scenario_bulk_status(
1956
1956
  typer.echo(result)
1957
1957
 
1958
1958
 
1959
+ @app.command("scenario-bulk-cancel")
1960
+ def scenario_bulk_cancel(
1961
+ job_id: str = typer.Argument(..., help="Bulk run job ID"),
1962
+ apply: bool = typer.Option(
1963
+ False, "--apply", help="Cancel the pending runs (default is a dry-run plan)"
1964
+ ),
1965
+ shop_id: str = typer.Option(None, "--shop-id", help="Override shop ID"),
1966
+ format: str = typer.Option(
1967
+ "text", "--format", "-f", help="Output format: text or json"
1968
+ ),
1969
+ ) -> None:
1970
+ """Cancel a stuck bulk run by deleting its queued/running scenario runs."""
1971
+ client = get_client(shop_id=shop_id)
1972
+ result = asyncio.run(
1973
+ tools.scenario_bulk_cancel(
1974
+ client, job_id=job_id, apply=apply, output_format=format
1975
+ )
1976
+ )
1977
+ typer.echo(result)
1978
+
1979
+
1959
1980
  @app.command("audit-metric-fields")
1960
1981
  def audit_metric_fields(
1961
1982
  is_active: bool = typer.Option(
@@ -6230,6 +6230,95 @@ async def scenario_bulk_status(
6230
6230
  return output
6231
6231
 
6232
6232
 
6233
+ _PENDING_RUN_STATUSES = ("queued", "running")
6234
+
6235
+
6236
+ async def scenario_bulk_cancel(
6237
+ client: AppliedClient,
6238
+ job_id: str,
6239
+ *,
6240
+ apply: bool = False,
6241
+ output_format: str = "text",
6242
+ ) -> str:
6243
+ """
6244
+ Cancel a stuck bulk scenario run job.
6245
+
6246
+ A bulk run can get stuck with runs left in 'queued' or 'running' forever, so
6247
+ the job never reports complete. This cancels the job by deleting those pending
6248
+ runs; completed and failed runs (and their result conversations) are preserved.
6249
+ Once no pending runs remain, the job reports as finished.
6250
+
6251
+ This replaces the manual workaround of deleting the 'agent-test-bulk-job:'
6252
+ key from browser localStorage.
6253
+
6254
+ Args:
6255
+ client: Authenticated AppliedClient
6256
+ job_id: The bulk run job UUID
6257
+ apply: If True, delete the pending runs; otherwise just report the plan
6258
+ output_format: 'text' (default) or 'json'
6259
+
6260
+ Returns:
6261
+ Summary of pending runs cancelled (or that would be cancelled).
6262
+ """
6263
+ try:
6264
+ runs = await client.list_scenario_runs(
6265
+ bulk_job_id=job_id, latest=False, limit=1000
6266
+ )
6267
+ except AppliedAPIError as e:
6268
+ return _format_error(e)
6269
+
6270
+ pending = [r for r in runs if str(r.get("status") or "") in _PENDING_RUN_STATUSES]
6271
+ result: dict[str, Any] = {
6272
+ "job_id": job_id,
6273
+ "apply": apply,
6274
+ "total_runs": len(runs),
6275
+ "pending_runs": len(pending),
6276
+ "terminal_runs": len(runs) - len(pending),
6277
+ "cancelled": 0,
6278
+ "errors": [],
6279
+ }
6280
+
6281
+ if not runs:
6282
+ result["message"] = (
6283
+ f"No runs found for job {job_id} — it may already be cleared or the "
6284
+ f"job id is invalid. Nothing to cancel."
6285
+ )
6286
+ return to_json(result) if output_format == "json" else result["message"]
6287
+
6288
+ if not pending:
6289
+ result["message"] = (
6290
+ f"Job {job_id} has no pending (queued/running) runs — nothing to "
6291
+ f"cancel. {result['terminal_runs']} run(s) already finished."
6292
+ )
6293
+ return to_json(result) if output_format == "json" else result["message"]
6294
+
6295
+ if not apply:
6296
+ result["message"] = (
6297
+ f"Would cancel {len(pending)} pending run(s) (queued/running) for job "
6298
+ f"{job_id}; {result['terminal_runs']} finished run(s) preserved. "
6299
+ f"Re-run with --apply to cancel."
6300
+ )
6301
+ return to_json(result) if output_format == "json" else result["message"]
6302
+
6303
+ cancelled = 0
6304
+ for run in pending:
6305
+ run_id = run.get("id")
6306
+ if not run_id:
6307
+ continue
6308
+ try:
6309
+ await client.delete_scenario_run(str(run_id))
6310
+ cancelled += 1
6311
+ except AppliedAPIError as e:
6312
+ result["errors"].append({"run_id": str(run_id), "error": str(e)})
6313
+ result["cancelled"] = cancelled
6314
+ result["message"] = (
6315
+ f"Cancelled {cancelled} pending run(s) for job {job_id}; "
6316
+ f"{result['terminal_runs']} finished run(s) preserved."
6317
+ + (f" {len(result['errors'])} error(s)." if result["errors"] else "")
6318
+ )
6319
+ return to_json(result) if output_format == "json" else result["message"]
6320
+
6321
+
6233
6322
  async def _load_audit_target_summaries(
6234
6323
  client: AppliedClient,
6235
6324
  ratings: list[dict[str, Any]],
@@ -119,6 +119,7 @@ DOMAIN_TOOL_RENAMES: dict[str, dict[str, str]] = {
119
119
  "scenario_run_delete": "scenarios_runs_delete",
120
120
  "scenario_bulk_run": "scenarios_bulk_run",
121
121
  "scenario_bulk_status": "scenarios_bulk_status",
122
+ "scenario_bulk_cancel": "scenarios_bulk_cancel",
122
123
  },
123
124
  "taxonomy": {
124
125
  "taxonomy_list": "taxonomy_items_list",
@@ -29,6 +29,11 @@ class ScenariosBulkRunInput(StrictInput):
29
29
  contact_override: dict[str, Any] | None = None
30
30
 
31
31
 
32
+ class ScenariosBulkCancelInput(StrictInput):
33
+ job_id: str
34
+ apply: bool = False
35
+
36
+
32
37
  class BenchmarksListInput(StrictInput):
33
38
  agent_id: str | None = None
34
39
  limit: int = 50
@@ -850,6 +855,38 @@ async def scenarios_bulk_status_handler(
850
855
  )
851
856
 
852
857
 
858
+ async def scenarios_bulk_cancel_handler(
859
+ client: AppliedClient,
860
+ params: ScenariosBulkCancelInput,
861
+ ) -> ToolResult[Any]:
862
+ from applied_cli import tools as legacy_tools
863
+
864
+ raw = await legacy_tools.scenario_bulk_cancel(
865
+ client,
866
+ job_id=params.job_id,
867
+ apply=params.apply,
868
+ output_format="json",
869
+ )
870
+ try:
871
+ data = json.loads(raw)
872
+ except (json.JSONDecodeError, TypeError):
873
+ return ToolResult(data={"message": raw}, summary=str(raw))
874
+
875
+ next_actions = []
876
+ if not params.apply and data.get("pending_runs"):
877
+ next_actions.append("Re-run with apply=true to cancel the pending runs.")
878
+ elif data.get("cancelled"):
879
+ next_actions.append(
880
+ f"Poll scenarios_bulk_status with job_id='{params.job_id}' to confirm "
881
+ f"the job now reports finished."
882
+ )
883
+ return ToolResult(
884
+ data=data,
885
+ summary=data.get("message", "Bulk cancel processed."),
886
+ next_actions=next_actions,
887
+ )
888
+
889
+
853
890
  def scenario_specs() -> list[ToolSpec]:
854
891
  return [
855
892
  ToolSpec(
@@ -1034,4 +1071,18 @@ def scenario_specs() -> list[ToolSpec]:
1034
1071
  read_write_mode="read",
1035
1072
  tags=["scenario_bulk_status", "native"],
1036
1073
  ),
1074
+ ToolSpec(
1075
+ name="scenarios_bulk_cancel",
1076
+ namespace="scenarios",
1077
+ description=(
1078
+ "Cancel a stuck bulk scenario run job by deleting its queued/"
1079
+ "running runs (completed/failed runs are preserved). Dry-run by "
1080
+ "default; set apply=true to cancel."
1081
+ ),
1082
+ input_model=ScenariosBulkCancelInput,
1083
+ output_model=None,
1084
+ handler=scenarios_bulk_cancel_handler,
1085
+ read_write_mode="write",
1086
+ tags=["scenario_bulk_cancel", "native"],
1087
+ ),
1037
1088
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -49,6 +49,7 @@ tests/test_conversation_tools.py
49
49
  tests/test_flow_tools.py
50
50
  tests/test_knowledge_content_tools.py
51
51
  tests/test_recovery.py
52
+ tests/test_scenario_bulk_cancel.py
52
53
  tests/test_toolkit_contract.py
53
54
  tests/test_v2_agents.py
54
55
  tests/test_v2_articles.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "applied-cli"
3
- version = "0.6.2"
3
+ version = "0.6.3"
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,87 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from applied_cli import tools
6
+
7
+
8
+ class FakeBulkClient:
9
+ def __init__(self, runs):
10
+ self._runs = runs
11
+ self.deleted = []
12
+
13
+ async def list_scenario_runs(
14
+ self, scenario_id=None, benchmark_id=None, bulk_job_id=None, latest=False,
15
+ limit=50,
16
+ ):
17
+ return list(self._runs)
18
+
19
+ async def delete_scenario_run(self, run_id):
20
+ self.deleted.append(run_id)
21
+
22
+
23
+ STUCK_RUNS = [
24
+ {"id": "r1", "status": "completed"},
25
+ {"id": "r2", "status": "queued"},
26
+ {"id": "r3", "status": "running"},
27
+ {"id": "r4", "status": "failed"},
28
+ ]
29
+
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_dry_run_reports_pending_without_deleting():
33
+ client = FakeBulkClient(STUCK_RUNS)
34
+ result = await tools.scenario_bulk_cancel(client, "job-1", output_format="json")
35
+ data = json.loads(result)
36
+ assert data["pending_runs"] == 2
37
+ assert data["terminal_runs"] == 2
38
+ assert data["cancelled"] == 0
39
+ assert client.deleted == [] # dry run deletes nothing
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_apply_deletes_only_pending_runs():
44
+ client = FakeBulkClient(STUCK_RUNS)
45
+ result = await tools.scenario_bulk_cancel(
46
+ client, "job-1", apply=True, output_format="json"
47
+ )
48
+ data = json.loads(result)
49
+ assert data["cancelled"] == 2
50
+ # Only the queued/running runs are deleted; completed/failed preserved.
51
+ assert set(client.deleted) == {"r2", "r3"}
52
+
53
+
54
+ @pytest.mark.asyncio
55
+ async def test_no_pending_runs_is_a_noop():
56
+ client = FakeBulkClient(
57
+ [{"id": "r1", "status": "completed"}, {"id": "r2", "status": "failed"}]
58
+ )
59
+ result = await tools.scenario_bulk_cancel(
60
+ client, "job-1", apply=True, output_format="text"
61
+ )
62
+ assert "nothing to" in result.lower()
63
+ assert client.deleted == []
64
+
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_unknown_job_reports_no_runs():
68
+ client = FakeBulkClient([])
69
+ result = await tools.scenario_bulk_cancel(client, "missing", output_format="text")
70
+ assert "No runs found" in result
71
+ assert client.deleted == []
72
+
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_v2_scenarios_bulk_cancel_handler_structured():
76
+ from applied_cli.v2.scenarios import (
77
+ ScenariosBulkCancelInput,
78
+ scenarios_bulk_cancel_handler,
79
+ )
80
+
81
+ client = FakeBulkClient(STUCK_RUNS)
82
+ result = await scenarios_bulk_cancel_handler(
83
+ client, ScenariosBulkCancelInput(job_id="job-1", apply=False)
84
+ )
85
+ assert result.data["pending_runs"] == 2
86
+ assert "apply=true" in " ".join(result.next_actions)
87
+ assert client.deleted == []
File without changes
File without changes