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.
- {applied_cli-0.4.0 → applied_cli-0.5.0}/PKG-INFO +1 -1
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli/__init__.py +3 -2
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli/client.py +181 -1
- applied_cli-0.5.0/applied_cli/tools.py +846 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli.egg-info/PKG-INFO +1 -1
- {applied_cli-0.4.0 → applied_cli-0.5.0}/pyproject.toml +1 -1
- applied_cli-0.4.0/applied_cli/tools.py +0 -210
- {applied_cli-0.4.0 → applied_cli-0.5.0}/README.md +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli/cli.py +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli/credentials.py +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli/formatters.py +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli.egg-info/SOURCES.txt +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli.egg-info/dependency_links.txt +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli.egg-info/entry_points.txt +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli.egg-info/requires.txt +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/applied_cli.egg-info/top_level.txt +0 -0
- {applied_cli-0.4.0 → applied_cli-0.5.0}/setup.cfg +0 -0
|
@@ -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.
|
|
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.
|
|
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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|