applied-cli 0.4.0__tar.gz → 0.5.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
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,8 +1,9 @@
1
1
  """Applied Labs CLI - shared client library for CLI and MCP server."""
2
2
 
3
+ from applied_cli import tools
3
4
  from applied_cli.client import AppliedClient
4
5
  from applied_cli.formatters import to_csv, to_json
5
6
 
6
- __version__ = "0.3.0"
7
+ __version__ = "0.5.0"
7
8
 
8
- __all__ = ["AppliedClient", "to_csv", "to_json", "__version__"]
9
+ __all__ = ["AppliedClient", "tools", "to_csv", "to_json", "__version__"]
@@ -44,11 +44,15 @@ class AppliedClient:
44
44
  elif method == "PATCH":
45
45
  resp = await client.patch(url, headers=headers, json=body or {})
46
46
  elif method == "DELETE":
47
- resp = await client.delete(url, headers=headers)
47
+ resp = await client.request(
48
+ "DELETE", url, headers=headers, json=body if body else None
49
+ )
48
50
  else:
49
51
  raise ValueError(f"Unsupported method: {method}")
50
52
 
51
53
  resp.raise_for_status()
54
+ if resp.status_code == 204:
55
+ return None
52
56
  return resp.json()
53
57
 
54
58
  def _normalize_response(self, data: Any) -> list[dict]:
@@ -185,3 +189,179 @@ class AppliedClient:
185
189
 
186
190
  data = await self._request("GET", "/v1/responses/", params=params)
187
191
  return self._normalize_response(data)
192
+
193
+ # -------------------------------------------------------------------------
194
+ # Flows
195
+ # -------------------------------------------------------------------------
196
+
197
+ async def list_flows(
198
+ self,
199
+ agent_id: str | None = None,
200
+ status: str | None = None,
201
+ with_graph: bool = False,
202
+ limit: int = 50,
203
+ ) -> list[dict]:
204
+ """List flows, optionally filtered by agent and status."""
205
+ params: dict[str, Any] = {"limit": limit}
206
+ if agent_id:
207
+ params["agent_id"] = agent_id
208
+ if status:
209
+ params["status"] = status
210
+ if with_graph:
211
+ params["with_graph"] = "true"
212
+
213
+ data = await self._request("GET", "/v1/flows/", params=params)
214
+ return self._normalize_response(data)
215
+
216
+ async def get_flow(self, flow_id: str) -> dict:
217
+ """Get a single flow with its graph (nodes and edges)."""
218
+ return await self._request("GET", f"/v1/flows/{flow_id}/")
219
+
220
+ async def create_flow(
221
+ self,
222
+ agent_id: str,
223
+ name: str,
224
+ flow_type: str = "conversational",
225
+ trigger: str = "llm.call",
226
+ description: str = "",
227
+ ) -> dict:
228
+ """Create a new flow."""
229
+ return await self._request(
230
+ "POST",
231
+ "/v1/flows/",
232
+ body={
233
+ "agent_id": agent_id,
234
+ "name": name,
235
+ "type": flow_type,
236
+ "trigger": trigger,
237
+ "description": description,
238
+ },
239
+ )
240
+
241
+ async def update_flow(self, flow_id: str, **kwargs: Any) -> dict:
242
+ """Update a flow's properties."""
243
+ return await self._request("PATCH", f"/v1/flows/{flow_id}/", body=kwargs)
244
+
245
+ async def delete_flow(self, flow_id: str) -> None:
246
+ """Delete a flow."""
247
+ await self._request("DELETE", f"/v1/flows/{flow_id}/")
248
+
249
+ # -------------------------------------------------------------------------
250
+ # Flow Nodes
251
+ # -------------------------------------------------------------------------
252
+
253
+ async def create_node(
254
+ self,
255
+ flow_id: str,
256
+ name: str,
257
+ metadata: dict[str, Any] | None = None,
258
+ description: str = "",
259
+ prompt: str = "",
260
+ ) -> dict:
261
+ """Create a node in a flow."""
262
+ return await self._request(
263
+ "POST",
264
+ f"/v1/flows/{flow_id}/nodes",
265
+ body={
266
+ "name": name,
267
+ "metadata": metadata or {},
268
+ "description": description,
269
+ "prompt": prompt,
270
+ },
271
+ )
272
+
273
+ async def update_node(self, flow_id: str, node_id: str, **kwargs: Any) -> dict:
274
+ """Update a node's properties."""
275
+ return await self._request(
276
+ "PATCH",
277
+ f"/v1/flows/{flow_id}/nodes",
278
+ body={"id": node_id, **kwargs},
279
+ )
280
+
281
+ async def delete_node(self, flow_id: str, node_id: str) -> None:
282
+ """Delete a node from a flow."""
283
+ await self._request(
284
+ "DELETE",
285
+ f"/v1/flows/{flow_id}/nodes",
286
+ body={"id": node_id},
287
+ )
288
+
289
+ # -------------------------------------------------------------------------
290
+ # Flow Edges
291
+ # -------------------------------------------------------------------------
292
+
293
+ async def create_edge(
294
+ self,
295
+ flow_id: str,
296
+ source_node_id: str,
297
+ target_node_id: str,
298
+ source_handle: str | None = None,
299
+ target_handle: str | None = None,
300
+ label: str | None = None,
301
+ ) -> dict:
302
+ """Create an edge connecting two nodes."""
303
+ body: dict[str, Any] = {
304
+ "source_node_id": source_node_id,
305
+ "target_node_id": target_node_id,
306
+ }
307
+ if source_handle:
308
+ body["source_result"] = source_handle
309
+ if target_handle:
310
+ body["target_argument"] = target_handle
311
+ if label:
312
+ body["label"] = label
313
+
314
+ return await self._request(
315
+ "POST",
316
+ f"/v1/flows/{flow_id}/edges",
317
+ body=body,
318
+ )
319
+
320
+ async def update_edge(self, flow_id: str, edge_id: str, **kwargs: Any) -> dict:
321
+ """Update an edge's properties."""
322
+ return await self._request(
323
+ "PATCH",
324
+ f"/v1/flows/{flow_id}/edges",
325
+ body={"id": edge_id, **kwargs},
326
+ )
327
+
328
+ async def delete_edge(self, flow_id: str, edge_id: str) -> None:
329
+ """Delete an edge from a flow."""
330
+ await self._request(
331
+ "DELETE",
332
+ f"/v1/flows/{flow_id}/edges",
333
+ body={"id": edge_id},
334
+ )
335
+
336
+ # -------------------------------------------------------------------------
337
+ # Flow Execution
338
+ # -------------------------------------------------------------------------
339
+
340
+ async def run_flow(
341
+ self,
342
+ flow_id: str,
343
+ trigger_data: dict[str, Any] | None = None,
344
+ ) -> dict:
345
+ """Execute a flow with trigger data."""
346
+ body = {"trigger": trigger_data or {}}
347
+ return await self._request("POST", f"/v1/flows/{flow_id}/run/", body=body)
348
+
349
+ async def list_flow_runs(
350
+ self,
351
+ flow_id: str | None = None,
352
+ status: str | None = None,
353
+ limit: int = 20,
354
+ ) -> list[dict]:
355
+ """List flow runs, optionally filtered by flow and status."""
356
+ params: dict[str, Any] = {"limit": limit, "ordering": "-started_at"}
357
+ if flow_id:
358
+ params["flow_id"] = flow_id
359
+ if status:
360
+ params["status"] = status
361
+
362
+ data = await self._request("GET", "/v1/flow-runs/", params=params)
363
+ return self._normalize_response(data)
364
+
365
+ async def get_flow_run(self, flow_run_id: str) -> dict:
366
+ """Get a flow run with execution spans and actions."""
367
+ return await self._request("GET", f"/v1/flow-runs/{flow_run_id}/")
@@ -0,0 +1,846 @@
1
+ """MCP tool implementations using AppliedClient.
2
+
3
+ These functions wrap the client methods with formatting logic,
4
+ suitable for both MCP tools and CLI commands.
5
+ """
6
+
7
+ from applied_cli.client import AppliedClient
8
+ from applied_cli.formatters import select_fields, to_csv, to_json
9
+
10
+
11
+ async def agent_list(
12
+ client: AppliedClient,
13
+ output_format: str = "csv",
14
+ ) -> str:
15
+ """
16
+ List all AI agents in the workspace.
17
+
18
+ Args:
19
+ client: Authenticated AppliedClient
20
+ output_format: 'csv' (default, token-efficient) or 'json'
21
+
22
+ Returns:
23
+ List of agents with id, name, modality, description
24
+ """
25
+ agents = await client.list_agents()
26
+ mapped = [
27
+ {
28
+ "id": a.get("id"),
29
+ "name": a.get("name"),
30
+ "modality": a.get("modality"),
31
+ "description": a.get("description", ""),
32
+ }
33
+ for a in agents
34
+ ]
35
+
36
+ if output_format == "csv":
37
+ return to_csv(mapped, ["id", "name", "modality", "description"])
38
+ return to_json(mapped)
39
+
40
+
41
+ async def taxonomy_list(
42
+ client: AppliedClient,
43
+ taxonomy_type: str = "all",
44
+ output_format: str = "csv",
45
+ ) -> str:
46
+ """
47
+ List conversation taxonomy: topics, intents, and flags.
48
+
49
+ Args:
50
+ client: Authenticated AppliedClient
51
+ taxonomy_type: 'all' (default), 'topics', 'intents', or 'flags'
52
+ output_format: 'csv' or 'json'
53
+
54
+ Returns:
55
+ Topics (categories), intents (sub-categories), flags (markers)
56
+ """
57
+ choices = await client.list_taxonomy(taxonomy_type)
58
+ items = [
59
+ {
60
+ "id": c.get("id"),
61
+ "name": c.get("name"),
62
+ "type": (
63
+ "flag"
64
+ if c.get("is_flag")
65
+ else ("intent" if c.get("parent_choice_id") else "topic")
66
+ ),
67
+ }
68
+ for c in choices
69
+ ]
70
+
71
+ if output_format == "csv":
72
+ return to_csv(items, ["id", "name", "type"])
73
+ return to_json(items)
74
+
75
+
76
+ async def conversation_query(
77
+ client: AppliedClient,
78
+ filters: dict | None = None,
79
+ fields: list[str] | None = None,
80
+ limit: int = 20,
81
+ output_format: str = "csv",
82
+ ) -> str:
83
+ """
84
+ Query conversations with filters and field selection.
85
+
86
+ Args:
87
+ client: Authenticated AppliedClient
88
+ filters: Filter conditions, e.g.:
89
+ {"resolution": "escalated"}
90
+ {"resolution": ["escalated", "soft"]}
91
+ {"created_at": {"gte": "2024-01-01", "lte": "2024-01-31"}}
92
+ fields: Fields to return, e.g. ["id", "title", "contact.email"]
93
+ limit: Max results (default 20, max 100)
94
+ output_format: 'csv' or 'json'
95
+
96
+ Returns:
97
+ Matching conversations
98
+ """
99
+ results = await client.query_conversations(filters=filters, limit=limit)
100
+
101
+ if fields:
102
+ results = select_fields(results, fields)
103
+
104
+ if output_format == "csv":
105
+ return f"# {len(results)} results\n" + to_csv(results, fields)
106
+ return to_json({"count": len(results), "results": results})
107
+
108
+
109
+ async def conversation_get(
110
+ client: AppliedClient,
111
+ conversation_id: str,
112
+ include_messages: bool = False,
113
+ message_limit: int = 50,
114
+ ) -> str:
115
+ """
116
+ Get a single conversation by ID.
117
+
118
+ Args:
119
+ client: Authenticated AppliedClient
120
+ conversation_id: The conversation UUID
121
+ include_messages: Include message transcript (default: False)
122
+ message_limit: Max messages to include (default: 50)
123
+
124
+ Returns:
125
+ Conversation details and optionally messages
126
+ """
127
+ conv = await client.get_conversation(conversation_id)
128
+ result = f"# Conversation {conversation_id}\n{to_json(conv)}"
129
+
130
+ if include_messages:
131
+ messages = await client.get_messages(conversation_id, limit=message_limit)
132
+ result += f"\n\n# Messages ({len(messages)})\n"
133
+ for m in messages:
134
+ role = m.get("role", "unknown")
135
+ content = str(m.get("content", ""))[:500]
136
+ result += f"\n[{role}] {content}"
137
+
138
+ return result
139
+
140
+
141
+ async def ticket_query(
142
+ client: AppliedClient,
143
+ filters: dict | None = None,
144
+ limit: int = 20,
145
+ output_format: str = "csv",
146
+ ) -> str:
147
+ """
148
+ Query tickets with filters.
149
+
150
+ Args:
151
+ client: Authenticated AppliedClient
152
+ filters: Filter conditions, e.g. {"status": "open"}
153
+ limit: Max results (default 20)
154
+ output_format: 'csv' or 'json'
155
+
156
+ Returns:
157
+ Matching tickets
158
+ """
159
+ results = await client.query_tickets(filters=filters, limit=limit)
160
+ mapped = [
161
+ {
162
+ "id": t.get("id"),
163
+ "name": t.get("name"),
164
+ "status": t.get("status"),
165
+ "priority": t.get("priority"),
166
+ }
167
+ for t in results
168
+ ]
169
+
170
+ if output_format == "csv":
171
+ return f"# {len(mapped)} tickets\n" + to_csv(
172
+ mapped, ["id", "name", "status", "priority"]
173
+ )
174
+ return to_json(mapped)
175
+
176
+
177
+ async def knowledge_list(
178
+ client: AppliedClient,
179
+ kb_type: str | None = None,
180
+ search: str | None = None,
181
+ limit: int = 50,
182
+ output_format: str = "csv",
183
+ ) -> str:
184
+ """
185
+ List knowledge base items (Q&A, escalation rules, context, exact responses).
186
+
187
+ Args:
188
+ client: Authenticated AppliedClient
189
+ kb_type: Filter by type - 'qa', 'context', 'escalate', 'exact'
190
+ search: Full-text search query
191
+ limit: Max results (default 50)
192
+ output_format: 'csv' or 'json'
193
+
194
+ Returns:
195
+ Knowledge base items
196
+ """
197
+ results = await client.list_knowledge(kb_type=kb_type, search=search, limit=limit)
198
+ mapped = [
199
+ {
200
+ "id": r.get("id"),
201
+ "type": r.get("type"),
202
+ "question": str(r.get("question", ""))[:100],
203
+ "active": r.get("active"),
204
+ }
205
+ for r in results
206
+ ]
207
+
208
+ if output_format == "csv":
209
+ return to_csv(mapped, ["id", "type", "question", "active"])
210
+ return to_json(mapped)
211
+
212
+
213
+ # -----------------------------------------------------------------------------
214
+ # Flow Management
215
+ # -----------------------------------------------------------------------------
216
+
217
+
218
+ async def flow_list(
219
+ client: AppliedClient,
220
+ agent_id: str | None = None,
221
+ status: str | None = None,
222
+ output_format: str = "csv",
223
+ ) -> str:
224
+ """
225
+ List flows for an agent.
226
+
227
+ Args:
228
+ client: Authenticated AppliedClient
229
+ agent_id: Filter by agent ID (recommended)
230
+ status: Filter by status - 'Active', 'Draft', or 'Archived'
231
+ output_format: 'csv' or 'json'
232
+
233
+ Returns:
234
+ List of flows with id, name, type, trigger, status, updated_at
235
+ """
236
+ flows = await client.list_flows(agent_id=agent_id, status=status)
237
+ mapped = [
238
+ {
239
+ "id": f.get("id"),
240
+ "name": f.get("name"),
241
+ "type": f.get("type"),
242
+ "trigger": f.get("trigger"),
243
+ "status": f.get("status"),
244
+ "updated_at": f.get("updated_at", "")[:19],
245
+ }
246
+ for f in flows
247
+ ]
248
+
249
+ if output_format == "csv":
250
+ return to_csv(mapped, ["id", "name", "type", "trigger", "status", "updated_at"])
251
+ return to_json(mapped)
252
+
253
+
254
+ async def flow_get(
255
+ client: AppliedClient,
256
+ flow_id: str,
257
+ ) -> str:
258
+ """
259
+ Get a flow with its full graph (nodes and edges).
260
+
261
+ Args:
262
+ client: Authenticated AppliedClient
263
+ flow_id: The flow UUID
264
+
265
+ Returns:
266
+ Flow metadata, nodes table, and edges table
267
+ """
268
+ flow = await client.get_flow(flow_id)
269
+ graph = flow.get("graph", {})
270
+ nodes = graph.get("nodes", [])
271
+ edges = graph.get("edges", [])
272
+
273
+ # Format header
274
+ result = f"# Flow: {flow.get('name')}\n"
275
+ result += f"id: {flow.get('id')}\n"
276
+ result += f"type: {flow.get('type')}\n"
277
+ result += f"trigger: {flow.get('trigger')}\n"
278
+ result += f"status: {flow.get('status')}\n"
279
+ if flow.get("description"):
280
+ result += f"description: {flow.get('description')}\n"
281
+ if flow.get("prompt"):
282
+ result += f"purpose: {flow.get('prompt')}\n"
283
+
284
+ # Format nodes
285
+ result += f"\n## Nodes ({len(nodes)})\n"
286
+ node_rows = [
287
+ {
288
+ "id": n.get("id"),
289
+ "type": n.get("type", n.get("data", {}).get("name", "")),
290
+ "description": n.get("data", {}).get("description", "")[:50],
291
+ }
292
+ for n in nodes
293
+ ]
294
+ result += to_csv(node_rows, ["id", "type", "description"])
295
+
296
+ # Format edges
297
+ result += f"\n## Edges ({len(edges)})\n"
298
+ edge_rows = [
299
+ {
300
+ "id": e.get("id"),
301
+ "source": e.get("source"),
302
+ "target": e.get("target"),
303
+ "source_handle": e.get("sourceHandle", ""),
304
+ "target_handle": e.get("targetHandle", ""),
305
+ "label": e.get("data", {}).get("label", ""),
306
+ }
307
+ for e in edges
308
+ ]
309
+ result += to_csv(
310
+ edge_rows, ["id", "source", "target", "source_handle", "target_handle", "label"]
311
+ )
312
+
313
+ return result
314
+
315
+
316
+ async def flow_create(
317
+ client: AppliedClient,
318
+ agent_id: str,
319
+ name: str,
320
+ flow_type: str = "conversational",
321
+ trigger: str = "llm.call",
322
+ description: str = "",
323
+ ) -> str:
324
+ """
325
+ Create a new flow.
326
+
327
+ Args:
328
+ client: Authenticated AppliedClient
329
+ agent_id: The agent to attach the flow to
330
+ name: Flow name
331
+ flow_type: 'conversational' (default) or 'operational'
332
+ trigger: 'llm.call' (conversation trigger), 'conversation.end', etc.
333
+ description: Optional flow description
334
+
335
+ Returns:
336
+ Created flow with auto-generated trigger and escalation nodes
337
+ """
338
+ flow = await client.create_flow(
339
+ agent_id=agent_id,
340
+ name=name,
341
+ flow_type=flow_type,
342
+ trigger=trigger,
343
+ description=description,
344
+ )
345
+
346
+ result = f"# Created Flow: {flow.get('name')}\n"
347
+ result += f"id: {flow.get('id')}\n"
348
+ result += f"type: {flow.get('type')}\n"
349
+ result += f"trigger: {flow.get('trigger')}\n"
350
+ result += f"status: {flow.get('status')}\n"
351
+ result += "\nUse flow_get to see the auto-generated nodes."
352
+
353
+ return result
354
+
355
+
356
+ async def flow_update(
357
+ client: AppliedClient,
358
+ flow_id: str,
359
+ name: str | None = None,
360
+ description: str | None = None,
361
+ prompt: str | None = None,
362
+ status: str | None = None,
363
+ ) -> str:
364
+ """
365
+ Update a flow's properties.
366
+
367
+ Args:
368
+ client: Authenticated AppliedClient
369
+ flow_id: The flow UUID
370
+ name: New name
371
+ description: New description
372
+ prompt: Flow purpose (used for routing)
373
+ status: 'Active', 'Draft', or 'Archived'
374
+
375
+ Returns:
376
+ Updated flow
377
+ """
378
+ updates = {}
379
+ if name is not None:
380
+ updates["name"] = name
381
+ if description is not None:
382
+ updates["description"] = description
383
+ if prompt is not None:
384
+ updates["prompt"] = prompt
385
+ if status is not None:
386
+ updates["status"] = status
387
+
388
+ flow = await client.update_flow(flow_id, **updates)
389
+
390
+ return f"# Updated Flow: {flow.get('name')}\nstatus: {flow.get('status')}"
391
+
392
+
393
+ async def flow_delete(
394
+ client: AppliedClient,
395
+ flow_id: str,
396
+ ) -> str:
397
+ """
398
+ Delete a flow.
399
+
400
+ Args:
401
+ client: Authenticated AppliedClient
402
+ flow_id: The flow UUID
403
+
404
+ Returns:
405
+ Success message
406
+ """
407
+ await client.delete_flow(flow_id)
408
+ return f"Flow {flow_id} deleted successfully."
409
+
410
+
411
+ # -----------------------------------------------------------------------------
412
+ # Node Management
413
+ # -----------------------------------------------------------------------------
414
+
415
+
416
+ async def flow_node_create(
417
+ client: AppliedClient,
418
+ flow_id: str,
419
+ executor_type: str,
420
+ description: str = "",
421
+ prompt: str = "",
422
+ metadata: dict | None = None,
423
+ ) -> str:
424
+ """
425
+ Add a node to a flow.
426
+
427
+ Args:
428
+ client: Authenticated AppliedClient
429
+ flow_id: The flow UUID
430
+ executor_type: Node type - 'completion', 'structured_completion',
431
+ 'branch', 'loop', 'http_request', 'code', 'memory', etc.
432
+ description: Node description
433
+ prompt: Prompt for LLM nodes
434
+ metadata: Node configuration as dict (input_schema, output_schema, etc.)
435
+
436
+ Returns:
437
+ Created node with generated ID
438
+ """
439
+ node = await client.create_node(
440
+ flow_id=flow_id,
441
+ name=executor_type,
442
+ description=description,
443
+ prompt=prompt,
444
+ metadata=metadata,
445
+ )
446
+
447
+ result = "# Created Node\n"
448
+ result += f"id: {node.get('id')}\n"
449
+ result += f"name: {node.get('name')}\n"
450
+ if description:
451
+ result += f"description: {description}\n"
452
+
453
+ return result
454
+
455
+
456
+ async def flow_node_update(
457
+ client: AppliedClient,
458
+ flow_id: str,
459
+ node_id: str,
460
+ description: str | None = None,
461
+ prompt: str | None = None,
462
+ metadata: dict | None = None,
463
+ ) -> str:
464
+ """
465
+ Update a node's configuration.
466
+
467
+ Args:
468
+ client: Authenticated AppliedClient
469
+ flow_id: The flow UUID
470
+ node_id: The node UUID
471
+ description: New description
472
+ prompt: New prompt
473
+ metadata: New metadata (merged with existing)
474
+
475
+ Returns:
476
+ Updated node
477
+ """
478
+ updates = {}
479
+ if description is not None:
480
+ updates["description"] = description
481
+ if prompt is not None:
482
+ updates["prompt"] = prompt
483
+ if metadata is not None:
484
+ updates["metadata"] = metadata
485
+
486
+ node = await client.update_node(flow_id, node_id, **updates)
487
+
488
+ return f"# Updated Node: {node.get('name')}\nid: {node.get('id')}"
489
+
490
+
491
+ async def flow_node_delete(
492
+ client: AppliedClient,
493
+ flow_id: str,
494
+ node_id: str,
495
+ ) -> str:
496
+ """
497
+ Remove a node from a flow.
498
+
499
+ Args:
500
+ client: Authenticated AppliedClient
501
+ flow_id: The flow UUID
502
+ node_id: The node UUID
503
+
504
+ Returns:
505
+ Success message
506
+ """
507
+ await client.delete_node(flow_id, node_id)
508
+ return f"Node {node_id} deleted successfully."
509
+
510
+
511
+ # -----------------------------------------------------------------------------
512
+ # Edge Management
513
+ # -----------------------------------------------------------------------------
514
+
515
+
516
+ async def flow_edge_create(
517
+ client: AppliedClient,
518
+ flow_id: str,
519
+ source_node_id: str,
520
+ target_node_id: str,
521
+ source_handle: str | None = None,
522
+ target_handle: str | None = None,
523
+ label: str | None = None,
524
+ ) -> str:
525
+ """
526
+ Connect two nodes with an edge.
527
+
528
+ Args:
529
+ client: Authenticated AppliedClient
530
+ flow_id: The flow UUID
531
+ source_node_id: Source node UUID
532
+ target_node_id: Target node UUID
533
+ source_handle: Output handle name (for branching)
534
+ target_handle: Input handle name (for mapping)
535
+ label: Edge label/condition
536
+
537
+ Returns:
538
+ Created edge
539
+ """
540
+ edge = await client.create_edge(
541
+ flow_id=flow_id,
542
+ source_node_id=source_node_id,
543
+ target_node_id=target_node_id,
544
+ source_handle=source_handle,
545
+ target_handle=target_handle,
546
+ label=label,
547
+ )
548
+
549
+ result = "# Created Edge\n"
550
+ result += f"id: {edge.get('id')}\n"
551
+ result += f"source: {edge.get('source')}\n"
552
+ result += f"target: {edge.get('target')}\n"
553
+
554
+ return result
555
+
556
+
557
+ async def flow_edge_update(
558
+ client: AppliedClient,
559
+ flow_id: str,
560
+ edge_id: str,
561
+ label: str | None = None,
562
+ metadata: dict | None = None,
563
+ ) -> str:
564
+ """
565
+ Update an edge's properties.
566
+
567
+ Args:
568
+ client: Authenticated AppliedClient
569
+ flow_id: The flow UUID
570
+ edge_id: The edge UUID
571
+ label: New label/condition
572
+ metadata: New metadata
573
+
574
+ Returns:
575
+ Updated edge
576
+ """
577
+ updates = {}
578
+ if label is not None:
579
+ updates["label"] = label
580
+ if metadata is not None:
581
+ updates["metadata"] = metadata
582
+
583
+ edge = await client.update_edge(flow_id, edge_id, **updates)
584
+
585
+ return f"# Updated Edge\nid: {edge.get('id')}"
586
+
587
+
588
+ async def flow_edge_delete(
589
+ client: AppliedClient,
590
+ flow_id: str,
591
+ edge_id: str,
592
+ ) -> str:
593
+ """
594
+ Remove an edge from a flow.
595
+
596
+ Args:
597
+ client: Authenticated AppliedClient
598
+ flow_id: The flow UUID
599
+ edge_id: The edge UUID
600
+
601
+ Returns:
602
+ Success message
603
+ """
604
+ await client.delete_edge(flow_id, edge_id)
605
+ return f"Edge {edge_id} deleted successfully."
606
+
607
+
608
+ # -----------------------------------------------------------------------------
609
+ # Flow Execution
610
+ # -----------------------------------------------------------------------------
611
+
612
+
613
+ async def flow_run(
614
+ client: AppliedClient,
615
+ flow_id: str,
616
+ trigger_data: dict | None = None,
617
+ ) -> str:
618
+ """
619
+ Execute a flow with test input.
620
+
621
+ Args:
622
+ client: Authenticated AppliedClient
623
+ flow_id: The flow UUID
624
+ trigger_data: Trigger input data, e.g.:
625
+ {"message": "Hello"}
626
+ {"message": "Help me", "contact": {"email": "user@example.com"}}
627
+ {"conversation_id": "uuid-of-existing-conversation"}
628
+
629
+ Returns:
630
+ Flow run ID, status, and actions summary
631
+ """
632
+ run = await client.run_flow(flow_id, trigger_data)
633
+
634
+ result = f"# Flow Run: {run.get('id')}\n"
635
+ result += f"status: {run.get('status')}\n"
636
+ result += f"started_at: {run.get('started_at')}\n"
637
+ result += f"ended_at: {run.get('ended_at')}\n"
638
+
639
+ actions = run.get("actions", [])
640
+ if actions:
641
+ result += f"\n## Actions ({len(actions)})\n"
642
+ action_rows = [
643
+ {
644
+ "node": a.get("tool", {}).get("name", ""),
645
+ "status": a.get("status"),
646
+ }
647
+ for a in actions
648
+ ]
649
+ result += to_csv(action_rows, ["node", "status"])
650
+
651
+ return result
652
+
653
+
654
+ async def flow_run_list(
655
+ client: AppliedClient,
656
+ flow_id: str | None = None,
657
+ status: str | None = None,
658
+ limit: int = 20,
659
+ output_format: str = "csv",
660
+ ) -> str:
661
+ """
662
+ List recent flow runs.
663
+
664
+ Args:
665
+ client: Authenticated AppliedClient
666
+ flow_id: Filter by flow UUID
667
+ status: Filter by status - 'Running', 'Success', 'Failed'
668
+ limit: Max results (default 20)
669
+ output_format: 'csv' or 'json'
670
+
671
+ Returns:
672
+ List of flow runs
673
+ """
674
+ runs = await client.list_flow_runs(flow_id=flow_id, status=status, limit=limit)
675
+ mapped = [
676
+ {
677
+ "id": r.get("id"),
678
+ "flow_name": r.get("flow", {}).get("name", ""),
679
+ "status": r.get("status"),
680
+ "started_at": r.get("started_at", "")[:19],
681
+ "duration": r.get("duration"),
682
+ }
683
+ for r in runs
684
+ ]
685
+
686
+ if output_format == "csv":
687
+ return to_csv(mapped, ["id", "flow_name", "status", "started_at", "duration"])
688
+ return to_json(mapped)
689
+
690
+
691
+ async def flow_run_get(
692
+ client: AppliedClient,
693
+ flow_run_id: str,
694
+ ) -> str:
695
+ """
696
+ Get a flow run with execution trace.
697
+
698
+ Args:
699
+ client: Authenticated AppliedClient
700
+ flow_run_id: The flow run UUID
701
+
702
+ Returns:
703
+ Run details, execution trace (spans), and errors
704
+ """
705
+ run = await client.get_flow_run(flow_run_id)
706
+
707
+ # Header
708
+ flow = run.get("flow", {})
709
+ result = f"# Flow Run: {run.get('id')}\n"
710
+ result += f"flow: {flow.get('name')} ({flow.get('id')})\n"
711
+ result += f"status: {run.get('status')}\n"
712
+ result += f"started_at: {run.get('started_at')}\n"
713
+ result += f"ended_at: {run.get('ended_at')}\n"
714
+ if run.get("duration"):
715
+ result += f"duration: {run.get('duration')}s\n"
716
+
717
+ # Execution trace from spans
718
+ spans = run.get("spans", [])
719
+ if spans:
720
+ result += f"\n## Execution Trace ({len(spans)} spans)\n"
721
+ for i, span in enumerate(spans, 1):
722
+ name = span.get("spanName", "")
723
+ duration = span.get("duration", 0)
724
+ status = span.get("statusCode", "")
725
+ status_msg = span.get("statusMessage", "")
726
+
727
+ status_str = "OK" if "OK" in str(status) else "ERROR"
728
+ result += f"{i}. [{name}] {duration:.2f}s - {status_str}"
729
+ if status_msg:
730
+ result += f" - {status_msg}"
731
+ result += "\n"
732
+
733
+ # Show executor output for executor_run spans
734
+ attrs = span.get("spanAttributes", {})
735
+ if name == "executor_run" and attrs.get("output"):
736
+ output = str(attrs.get("output", ""))[:200]
737
+ result += f" Output: {output}\n"
738
+
739
+ # Actions
740
+ actions = run.get("actions", [])
741
+ if actions:
742
+ result += f"\n## Actions ({len(actions)})\n"
743
+ action_rows = [
744
+ {
745
+ "node": a.get("tool", {}).get("name", ""),
746
+ "status": a.get("status"),
747
+ "cost": a.get("cost", 0),
748
+ }
749
+ for a in actions
750
+ ]
751
+ result += to_csv(action_rows, ["node", "status", "cost"])
752
+
753
+ # Errors
754
+ errors = [s for s in spans if "ERROR" in str(s.get("statusCode", ""))]
755
+ if errors:
756
+ result += "\n## Errors\n"
757
+ for err in errors:
758
+ result += (
759
+ f"Node: {err.get('spanAttributes', {}).get('toolName', 'unknown')}\n"
760
+ )
761
+ result += f"Message: {err.get('statusMessage', 'Unknown error')}\n\n"
762
+
763
+ return result
764
+
765
+
766
+ async def executor_list(
767
+ client: AppliedClient,
768
+ output_format: str = "csv",
769
+ ) -> str:
770
+ """
771
+ List available node types (executors).
772
+
773
+ Args:
774
+ client: Authenticated AppliedClient (not used, list is hardcoded)
775
+ output_format: 'csv' or 'json'
776
+
777
+ Returns:
778
+ Available executor types with descriptions
779
+ """
780
+ # Hardcoded list of common executors
781
+ executors = [
782
+ {
783
+ "name": "completion",
784
+ "description": "LLM completion node - generates free-form text response",
785
+ "use_case": "Generate responses, summaries, or any text output",
786
+ },
787
+ {
788
+ "name": "structured_completion",
789
+ "description": "LLM completion with structured JSON output",
790
+ "use_case": "Extract data, classify, or generate typed responses",
791
+ },
792
+ {
793
+ "name": "branch",
794
+ "description": "Conditional branching based on LLM decision",
795
+ "use_case": "Route flow based on user intent or conditions",
796
+ },
797
+ {
798
+ "name": "loop",
799
+ "description": "Loop over a list and execute nodes for each item",
800
+ "use_case": "Process multiple items, batch operations",
801
+ },
802
+ {
803
+ "name": "http_request",
804
+ "description": "Make HTTP API calls",
805
+ "use_case": "Integrate with external APIs",
806
+ },
807
+ {
808
+ "name": "code",
809
+ "description": "Execute Python code",
810
+ "use_case": "Data transformation, calculations",
811
+ },
812
+ {
813
+ "name": "memory",
814
+ "description": "Store/retrieve data in conversation memory",
815
+ "use_case": "Remember user preferences, context",
816
+ },
817
+ {
818
+ "name": "global",
819
+ "description": "Global handler for escalation or failure",
820
+ "use_case": "Catch-all for errors or escalation intents",
821
+ },
822
+ {
823
+ "name": "end_flow",
824
+ "description": "Explicitly end the flow",
825
+ "use_case": "Terminate flow execution",
826
+ },
827
+ {
828
+ "name": "_escalate_conversation",
829
+ "description": "Escalate conversation to human agent",
830
+ "use_case": "Hand off to support team",
831
+ },
832
+ {
833
+ "name": "handoff",
834
+ "description": "Hand off to another flow or agent",
835
+ "use_case": "Transfer to specialized flow",
836
+ },
837
+ {
838
+ "name": "search",
839
+ "description": "Search knowledge base",
840
+ "use_case": "Find relevant documentation or Q&A",
841
+ },
842
+ ]
843
+
844
+ if output_format == "csv":
845
+ return to_csv(executors, ["name", "description", "use_case"])
846
+ return to_json(executors)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: applied-cli
3
- Version: 0.4.0
3
+ Version: 0.5.0
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.4.0"
3
+ version = "0.5.0"
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"
@@ -1,210 +0,0 @@
1
- """MCP tool implementations using AppliedClient.
2
-
3
- These functions wrap the client methods with formatting logic,
4
- suitable for both MCP tools and CLI commands.
5
- """
6
-
7
- from applied_cli.client import AppliedClient
8
- from applied_cli.formatters import select_fields, to_csv, to_json
9
-
10
-
11
- async def agent_list(
12
- client: AppliedClient,
13
- output_format: str = "csv",
14
- ) -> str:
15
- """
16
- List all AI agents in the workspace.
17
-
18
- Args:
19
- client: Authenticated AppliedClient
20
- output_format: 'csv' (default, token-efficient) or 'json'
21
-
22
- Returns:
23
- List of agents with id, name, modality, description
24
- """
25
- agents = await client.list_agents()
26
- mapped = [
27
- {
28
- "id": a.get("id"),
29
- "name": a.get("name"),
30
- "modality": a.get("modality"),
31
- "description": a.get("description", ""),
32
- }
33
- for a in agents
34
- ]
35
-
36
- if output_format == "csv":
37
- return to_csv(mapped, ["id", "name", "modality", "description"])
38
- return to_json(mapped)
39
-
40
-
41
- async def taxonomy_list(
42
- client: AppliedClient,
43
- taxonomy_type: str = "all",
44
- output_format: str = "csv",
45
- ) -> str:
46
- """
47
- List conversation taxonomy: topics, intents, and flags.
48
-
49
- Args:
50
- client: Authenticated AppliedClient
51
- taxonomy_type: 'all' (default), 'topics', 'intents', or 'flags'
52
- output_format: 'csv' or 'json'
53
-
54
- Returns:
55
- Topics (categories), intents (sub-categories), flags (markers)
56
- """
57
- choices = await client.list_taxonomy(taxonomy_type)
58
- items = [
59
- {
60
- "id": c.get("id"),
61
- "name": c.get("name"),
62
- "type": (
63
- "flag"
64
- if c.get("is_flag")
65
- else ("intent" if c.get("parent_choice_id") else "topic")
66
- ),
67
- }
68
- for c in choices
69
- ]
70
-
71
- if output_format == "csv":
72
- return to_csv(items, ["id", "name", "type"])
73
- return to_json(items)
74
-
75
-
76
- async def conversation_query(
77
- client: AppliedClient,
78
- filters: dict | None = None,
79
- fields: list[str] | None = None,
80
- limit: int = 20,
81
- output_format: str = "csv",
82
- ) -> str:
83
- """
84
- Query conversations with filters and field selection.
85
-
86
- Args:
87
- client: Authenticated AppliedClient
88
- filters: Filter conditions, e.g.:
89
- {"resolution": "escalated"}
90
- {"resolution": ["escalated", "soft"]}
91
- {"created_at": {"gte": "2024-01-01", "lte": "2024-01-31"}}
92
- fields: Fields to return, e.g. ["id", "title", "contact.email"]
93
- limit: Max results (default 20, max 100)
94
- output_format: 'csv' or 'json'
95
-
96
- Returns:
97
- Matching conversations
98
- """
99
- results = await client.query_conversations(filters=filters, limit=limit)
100
-
101
- if fields:
102
- results = select_fields(results, fields)
103
-
104
- if output_format == "csv":
105
- return f"# {len(results)} results\n" + to_csv(results, fields)
106
- return to_json({"count": len(results), "results": results})
107
-
108
-
109
- async def conversation_get(
110
- client: AppliedClient,
111
- conversation_id: str,
112
- include_messages: bool = False,
113
- message_limit: int = 50,
114
- ) -> str:
115
- """
116
- Get a single conversation by ID.
117
-
118
- Args:
119
- client: Authenticated AppliedClient
120
- conversation_id: The conversation UUID
121
- include_messages: Include message transcript (default: False)
122
- message_limit: Max messages to include (default: 50)
123
-
124
- Returns:
125
- Conversation details and optionally messages
126
- """
127
- conv = await client.get_conversation(conversation_id)
128
- result = f"# Conversation {conversation_id}\n{to_json(conv)}"
129
-
130
- if include_messages:
131
- messages = await client.get_messages(conversation_id, limit=message_limit)
132
- result += f"\n\n# Messages ({len(messages)})\n"
133
- for m in messages:
134
- role = m.get("role", "unknown")
135
- content = str(m.get("content", ""))[:500]
136
- result += f"\n[{role}] {content}"
137
-
138
- return result
139
-
140
-
141
- async def ticket_query(
142
- client: AppliedClient,
143
- filters: dict | None = None,
144
- limit: int = 20,
145
- output_format: str = "csv",
146
- ) -> str:
147
- """
148
- Query tickets with filters.
149
-
150
- Args:
151
- client: Authenticated AppliedClient
152
- filters: Filter conditions, e.g. {"status": "open"}
153
- limit: Max results (default 20)
154
- output_format: 'csv' or 'json'
155
-
156
- Returns:
157
- Matching tickets
158
- """
159
- results = await client.query_tickets(filters=filters, limit=limit)
160
- mapped = [
161
- {
162
- "id": t.get("id"),
163
- "name": t.get("name"),
164
- "status": t.get("status"),
165
- "priority": t.get("priority"),
166
- }
167
- for t in results
168
- ]
169
-
170
- if output_format == "csv":
171
- return f"# {len(mapped)} tickets\n" + to_csv(
172
- mapped, ["id", "name", "status", "priority"]
173
- )
174
- return to_json(mapped)
175
-
176
-
177
- async def knowledge_list(
178
- client: AppliedClient,
179
- kb_type: str | None = None,
180
- search: str | None = None,
181
- limit: int = 50,
182
- output_format: str = "csv",
183
- ) -> str:
184
- """
185
- List knowledge base items (Q&A, escalation rules, context, exact responses).
186
-
187
- Args:
188
- client: Authenticated AppliedClient
189
- kb_type: Filter by type - 'qa', 'context', 'escalate', 'exact'
190
- search: Full-text search query
191
- limit: Max results (default 50)
192
- output_format: 'csv' or 'json'
193
-
194
- Returns:
195
- Knowledge base items
196
- """
197
- results = await client.list_knowledge(kb_type=kb_type, search=search, limit=limit)
198
- mapped = [
199
- {
200
- "id": r.get("id"),
201
- "type": r.get("type"),
202
- "question": str(r.get("question", ""))[:100],
203
- "active": r.get("active"),
204
- }
205
- for r in results
206
- ]
207
-
208
- if output_format == "csv":
209
- return to_csv(mapped, ["id", "type", "question", "active"])
210
- return to_json(mapped)
File without changes
File without changes