applied-cli 0.5.68__tar.gz → 0.5.70__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 (27) hide show
  1. {applied_cli-0.5.68 → applied_cli-0.5.70}/PKG-INFO +1 -1
  2. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/__init__.py +1 -1
  3. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/agent_scoped_flows.py +10 -13
  4. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/flow_helpers.py +3 -0
  5. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/tools.py +63 -0
  6. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli.egg-info/PKG-INFO +1 -1
  7. {applied_cli-0.5.68 → applied_cli-0.5.70}/pyproject.toml +1 -1
  8. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_agent_scoped_flows.py +11 -4
  9. {applied_cli-0.5.68 → applied_cli-0.5.70}/README.md +0 -0
  10. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/cli.py +0 -0
  11. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/client.py +0 -0
  12. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/conversation_lookup.py +0 -0
  13. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/conversations.py +0 -0
  14. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/credentials.py +0 -0
  15. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli/formatters.py +0 -0
  16. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli.egg-info/SOURCES.txt +0 -0
  17. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli.egg-info/dependency_links.txt +0 -0
  18. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli.egg-info/entry_points.txt +0 -0
  19. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli.egg-info/requires.txt +0 -0
  20. {applied_cli-0.5.68 → applied_cli-0.5.70}/applied_cli.egg-info/top_level.txt +0 -0
  21. {applied_cli-0.5.68 → applied_cli-0.5.70}/setup.cfg +0 -0
  22. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_audit_tools.py +0 -0
  23. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_benchmark_scenario_tools.py +0 -0
  24. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_cli.py +0 -0
  25. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_client.py +0 -0
  26. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_conversation_tools.py +0 -0
  27. {applied_cli-0.5.68 → applied_cli-0.5.70}/tests/test_flow_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.5.68
3
+ Version: 0.5.70
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -4,6 +4,6 @@ from applied_cli import tools
4
4
  from applied_cli.client import AppliedClient
5
5
  from applied_cli.formatters import to_csv, to_json
6
6
 
7
- __version__ = "0.5.68"
7
+ __version__ = "0.5.70"
8
8
 
9
9
  __all__ = ["AppliedClient", "tools", "to_csv", "to_json", "__version__"]
@@ -42,9 +42,7 @@ def _parse_json_option(value: str | None, *, option_name: str) -> dict[str, Any]
42
42
  ) from exc
43
43
 
44
44
  if not isinstance(parsed, dict):
45
- raise AgentScopedFlowCLIError(
46
- f"{option_name} must decode to a JSON object."
47
- )
45
+ raise AgentScopedFlowCLIError(f"{option_name} must decode to a JSON object.")
48
46
 
49
47
  return parsed
50
48
 
@@ -123,7 +121,9 @@ async def _handle_flow_delete(
123
121
  get_client: Callable[[str | None], AppliedClient],
124
122
  ) -> int:
125
123
  client = get_client(args.shop_id)
126
- flow = await _get_flow_for_agent(client, agent_id=args.agent_id, flow_id=args.flow_id)
124
+ flow = await _get_flow_for_agent(
125
+ client, agent_id=args.agent_id, flow_id=args.flow_id
126
+ )
127
127
  await client.delete_flow(args.flow_id)
128
128
  if args.format == "json":
129
129
  pass
@@ -139,12 +139,6 @@ async def _handle_node_create(
139
139
  client = get_client(args.shop_id)
140
140
  await _get_flow_for_agent(client, agent_id=args.agent_id, flow_id=args.flow_id)
141
141
 
142
- if args.guardrail:
143
- raise AgentScopedFlowCLIError(
144
- "--guardrail is not supported on node create because the API does not "
145
- "persist it. Create the node first, then run node update --guardrail."
146
- )
147
-
148
142
  node = await client.create_node(
149
143
  flow_id=args.flow_id,
150
144
  name=normalize_executor_type(args.executor_type),
@@ -193,11 +187,12 @@ async def _handle_node_delete(
193
187
  get_client: Callable[[str | None], AppliedClient],
194
188
  ) -> int:
195
189
  client = get_client(args.shop_id)
196
- flow = await _get_flow_for_agent(client, agent_id=args.agent_id, flow_id=args.flow_id)
190
+ flow = await _get_flow_for_agent(
191
+ client, agent_id=args.agent_id, flow_id=args.flow_id
192
+ )
197
193
 
198
194
  graph = flow.get("graph") or {}
199
195
  nodes = graph.get("nodes") or []
200
- edges = graph.get("edges") or []
201
196
  node = next((item for item in nodes if item.get("id") == args.node_id), None)
202
197
  if node is None:
203
198
  raise AgentScopedFlowCLIError(
@@ -265,7 +260,9 @@ async def _handle_edge_delete(
265
260
  get_client: Callable[[str | None], AppliedClient],
266
261
  ) -> int:
267
262
  client = get_client(args.shop_id)
268
- flow = await _get_flow_for_agent(client, agent_id=args.agent_id, flow_id=args.flow_id)
263
+ flow = await _get_flow_for_agent(
264
+ client, agent_id=args.agent_id, flow_id=args.flow_id
265
+ )
269
266
 
270
267
  graph = flow.get("graph") or {}
271
268
  edges = graph.get("edges") or []
@@ -16,6 +16,7 @@ _CANONICAL_EXECUTOR_TYPES: tuple[str, ...] = (
16
16
  "code",
17
17
  "http_request",
18
18
  "loop",
19
+ "agent_loop",
19
20
  "memory",
20
21
  "search",
21
22
  "recommend",
@@ -38,6 +39,7 @@ _EXECUTOR_UI_LABELS: dict[str, str] = {
38
39
  "code": "Code",
39
40
  "http_request": "HTTP Request",
40
41
  "loop": "Loop",
42
+ "agent_loop": "Agent Loop",
41
43
  "memory": "Memory",
42
44
  "search": "Search",
43
45
  "recommend": "Recommend",
@@ -62,6 +64,7 @@ _EXECUTOR_ALIASES: dict[str, tuple[str, ...]] = {
62
64
  "structured completion",
63
65
  ),
64
66
  "flow": ("run flow", "run_flow", "subflow"),
67
+ "agent_loop": ("agent loop", "planner loop", "tool belt", "agentic loop"),
65
68
  "end_flow": (
66
69
  "end flow return",
67
70
  "end flow and return",
@@ -2383,6 +2383,39 @@ async def flow_node_create(
2383
2383
  '"position": {"x": 0, "y": 600}}\''
2384
2384
  )
2385
2385
 
2386
+ # Validate agent_loop metadata
2387
+ if canonical_executor_type == "agent_loop":
2388
+ tool_belt = metadata.get("tool_belt")
2389
+ if not tool_belt:
2390
+ return (
2391
+ "❌ Error: agent_loop node requires a non-empty 'tool_belt' in metadata.\n\n"
2392
+ "💡 Required fields:\n"
2393
+ ' - tool_belt: list of {"action_name", "flow_id", "description"} entries\n'
2394
+ " Each entry maps a planner action name to an operational subflow.\n"
2395
+ " - model: \"fast\" or \"powerful\" (optional, default \"fast\")\n"
2396
+ " - content: string - LLM context block (optional; auto-built from\n"
2397
+ " the conversation trigger when empty and include_trigger_context=true)\n"
2398
+ " - include_trigger_context: bool (optional, default true)\n"
2399
+ " - planner_schema: list of SchemaField dicts for extra planner outputs\n"
2400
+ " - max_steps: int >= 1 (optional, default 10)\n"
2401
+ " - max_parallel_actions: int >= 1 (optional, default 3)\n\n"
2402
+ "Example:\n"
2403
+ 'metadata=\'{"model": "fast", "max_steps": 8, "tool_belt": ['
2404
+ '{"action_name": "lookup_order", "flow_id": "<subflow-uuid>", '
2405
+ '"description": "Fetch an order by number"}], '
2406
+ '"position": {"x": 0, "y": 300}}\''
2407
+ )
2408
+ if not isinstance(tool_belt, list) or not all(
2409
+ isinstance(entry, dict)
2410
+ and entry.get("action_name")
2411
+ and entry.get("flow_id")
2412
+ for entry in tool_belt
2413
+ ):
2414
+ return (
2415
+ "❌ Error: agent_loop 'tool_belt' must be a list of objects, "
2416
+ "each with 'action_name' and 'flow_id'."
2417
+ )
2418
+
2386
2419
  # Validate mutate_ticket metadata
2387
2420
  is_mutate_ticket = canonical_executor_type == "mutate_ticket"
2388
2421
  ticket_missing_name = not metadata or "name" not in metadata
@@ -3766,6 +3799,36 @@ async def executor_list(
3766
3799
  },
3767
3800
  "output_fields": ["results", "count"],
3768
3801
  },
3802
+ {
3803
+ "name": "agent_loop",
3804
+ "ui_label": "Agent Loop",
3805
+ "aliases": executor_aliases("agent_loop"),
3806
+ "description": "Planner-driven loop that dispatches tool-belt subflows until it finishes",
3807
+ "use_case": (
3808
+ "Let an LLM planner iteratively choose which operational "
3809
+ "subflows to call (in parallel where safe) before replying. "
3810
+ "Each tool-belt entry maps an action name to an operational flow; "
3811
+ "the planner emits one or more calls per step and ends with 'finish'."
3812
+ ),
3813
+ "metadata_fields": {
3814
+ "model": "string - 'fast' or 'powerful' (default 'fast')",
3815
+ "content": "string - LLM context block; leave empty to auto-build from the conversation trigger",
3816
+ "include_trigger_context": "bool - auto-assemble trigger context when content is empty (default true)",
3817
+ "tool_belt": (
3818
+ "array (required) - list of {action_name, flow_id, description} entries; "
3819
+ "each flow_id must be an operational subflow"
3820
+ ),
3821
+ "planner_schema": "array - SchemaField dicts surfaced on the loop's output and per-step payload",
3822
+ "max_steps": "int >= 1 - hard cap on planner iterations (default 10)",
3823
+ "max_parallel_actions": "int >= 1 - max actions the planner may dispatch per step (default 3)",
3824
+ },
3825
+ "output_fields": [
3826
+ "outcome",
3827
+ "steps_taken",
3828
+ "max_steps_reached",
3829
+ "(plus fields defined by planner_schema)",
3830
+ ],
3831
+ },
3769
3832
  {
3770
3833
  "name": "memory",
3771
3834
  "ui_label": "Memory",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.5.68
3
+ Version: 0.5.70
4
4
  Summary: CLI and shared client library for Applied Labs AI support agents
5
5
  Author: Applied Labs
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "applied-cli"
3
- version = "0.5.68"
3
+ version = "0.5.70"
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"
@@ -282,7 +282,7 @@ async def test_node_create_prints_raw_json(capsys):
282
282
 
283
283
 
284
284
  @pytest.mark.asyncio
285
- async def test_node_create_rejects_guardrail_on_create():
285
+ async def test_node_create_accepts_guardrail_on_create():
286
286
  client = StubClient()
287
287
  args = _parse_args(
288
288
  [
@@ -299,8 +299,11 @@ async def test_node_create_rejects_guardrail_on_create():
299
299
  ]
300
300
  )
301
301
 
302
- with pytest.raises(AgentScopedFlowCLIError, match="not supported on node create"):
303
- await _handle_node_create(args, lambda _shop_id: client)
302
+ exit_code = await _handle_node_create(args, lambda _shop_id: client)
303
+
304
+ assert exit_code == 0
305
+ assert client.created_node_args is not None
306
+ assert client.created_node_args["guardrail"] == "Be careful"
304
307
 
305
308
 
306
309
  @pytest.mark.asyncio
@@ -347,7 +350,11 @@ async def test_node_update_prints_raw_json(capsys):
347
350
 
348
351
  assert exit_code == 0
349
352
  assert client.updated_node_args == ("flow-1", "node-1", {"prompt": "hi"})
350
- assert json.loads(capsys.readouterr().out) == {**client.node, "id": "node-1", "prompt": "hi"}
353
+ assert json.loads(capsys.readouterr().out) == {
354
+ **client.node,
355
+ "id": "node-1",
356
+ "prompt": "hi",
357
+ }
351
358
 
352
359
 
353
360
  @pytest.mark.asyncio
File without changes
File without changes